From 28508ebe368f5f6058cfadbf14892a32fb050a98 Mon Sep 17 00:00:00 2001 From: Cristina Ferrian Date: Fri, 31 May 2024 15:28:22 +0200 Subject: [PATCH 1/8] refactor(ui): changed accept/reject grade confirm messages --- assets/translations/en.json | 2 +- assets/translations/it.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 653382b7..db6fa72a 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -617,7 +617,7 @@ "trainingOffer": "Training offer" }, "provisionalGradeScreen": { - "acceptGradeConfirmMessage": "By accepting this evaluation you will no longer be able to change your decision", + "acceptGradeConfirmMessage": "By requesting immediate registration, the evaluation will be recorded in your transcript and you will no longer be able to change your decision", "acceptGradeCta": "Request immediate registration", "acceptGradeFeedback": "The evaluation has been recorded, it will appear in the transcript", "contactProfessorCta": "Contact the teacher", diff --git a/assets/translations/it.json b/assets/translations/it.json index fd165e6a..d67dc93c 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -617,11 +617,11 @@ "trainingOffer": "Offerta formativa" }, "provisionalGradeScreen": { - "acceptGradeConfirmMessage": "Accettando il voto non potrai più cambiare la tua decisione", + "acceptGradeConfirmMessage": "Richiedendo la registrazione immediata la valutazione verrà inserita in libretto e non potrai più cambiare la tua decisione", "acceptGradeCta": "Richiedi la registrazione immediata", "acceptGradeFeedback": "Valutazione registrata, verrà visualizzata nel libretto", "contactProfessorCta": "Contatta il docente", - "rejectGradeConfirmMessage": "Rifiutando il voto non potrai più cambiare la tua decisione", + "rejectGradeConfirmMessage": "Rifiutando la valutazione non potrai più cambiare la tua decisione", "rejectGradeCta": "Rifiuta la valutazione", "rejectGradeFeedback": "Valutazione rifiutata, verrà registrata nelle prossime ore", "title": "Valutazione" From 03b91049e31033e228a09cee384222d1edcd24c0 Mon Sep 17 00:00:00 2001 From: Fabrizio Costa Medich <134924838+FabrizioCostaMedich@users.noreply.github.com> Date: Mon, 17 Jun 2024 16:51:51 +0200 Subject: [PATCH 2/8] fix(exams): new style exam screens (#500) * fix(exams): fix style on exams screens * fix(exams): do not show buttons when exam is not passed * fix(exams): fix checks on canBeRejected and canBeAccepted loot boxes * fix(exams): fix button position on ios and android * fix(exams): fix position of button by platform --------- Co-authored-by: Emanuele Coricciati --- assets/translations/en.json | 5 +- assets/translations/it.json | 5 +- lib/ui/components/CtaButton.tsx | 18 +- lib/ui/components/CtaButtonContainer.tsx | 1 + src/core/components/TextWithLinks.tsx | 13 +- src/core/queries/authHooks.ts | 2 +- .../components/ProvisionalGradeListItem.tsx | 15 +- .../transcript/hooks/useGetRejectionTime.tsx | 6 +- .../screens/ProvisionalGradeScreen.tsx | 157 ++++++++++++------ 9 files changed, 141 insertions(+), 81 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index db6fa72a..1f01a327 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -622,7 +622,7 @@ "acceptGradeFeedback": "The evaluation has been recorded, it will appear in the transcript", "contactProfessorCta": "Contact the teacher", "rejectGradeConfirmMessage": "By rejecting this evaluation you will no longer be able to change your decision", - "rejectGradeCta": "Reject the evaluation", + "rejectGradeCta": "Reject the evaluation by:
{{hours}}", "rejectGradeFeedback": "Evaluation rejected, it will be recorded in the next few hours", "title": "Evaluation" }, @@ -707,13 +707,14 @@ "title": "Evaluation" }, "transcriptGradesScreen": { + "autoRegistration": "Automatic registration in:", "emptyState": "You haven't taken any exams", "expiredCountdown": "Expired", "provisionalEmptyState": "There are no provisional grades", "provisionalTitle": "Provisional", "recordedTitle": "Recorded", "rejectedSubtitle": "Rejected on {{-date}} at {{-time}}", - "rejectionCountdown": "Rejectable by:", + "rejectionCountdown": "Rejectable by: {{hours}}", "title": "Grades", "total": "There are {{total}} grades" }, diff --git a/assets/translations/it.json b/assets/translations/it.json index d67dc93c..8da237c1 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -622,7 +622,7 @@ "acceptGradeFeedback": "Valutazione registrata, verrà visualizzata nel libretto", "contactProfessorCta": "Contatta il docente", "rejectGradeConfirmMessage": "Rifiutando la valutazione non potrai più cambiare la tua decisione", - "rejectGradeCta": "Rifiuta la valutazione", + "rejectGradeCta": "Rifiuta la valutazione entro:
{{hours}}", "rejectGradeFeedback": "Valutazione rifiutata, verrà registrata nelle prossime ore", "title": "Valutazione" }, @@ -707,13 +707,14 @@ "title": "Valutazione" }, "transcriptGradesScreen": { + "autoRegistration": "Registrazione automatica tra:", "emptyState": "Non hai sostenuto nessun esame", "expiredCountdown": "Scaduto", "provisionalEmptyState": "Non ci sono valutazioni provvisorie", "provisionalTitle": "Provvisorie", "recordedTitle": "Registrate", "rejectedSubtitle": "Rifiutato il {{-date}} alle {{-time}}", - "rejectionCountdown": "Rifiutabile entro:", + "rejectionCountdown": "Rifiutabile entro: {{hours}}", "title": "Valutazioni", "total": "Sono presenti {{total}} valutazioni" }, diff --git a/lib/ui/components/CtaButton.tsx b/lib/ui/components/CtaButton.tsx index a9020467..1426b1e5 100644 --- a/lib/ui/components/CtaButton.tsx +++ b/lib/ui/components/CtaButton.tsx @@ -18,6 +18,7 @@ import { useTheme } from '@lib/ui/hooks/useTheme'; import { Theme } from '@lib/ui/types/Theme'; import { shadeColor } from '@lib/ui/utils/colors'; +import { TextWithLinks } from '../../../src/core/components/TextWithLinks'; import { useFeedbackContext } from '../../../src/core/contexts/FeedbackContext'; import { useSafeBottomBarHeight } from '../../../src/core/hooks/useSafeBottomBarHeight'; @@ -54,7 +55,8 @@ export const CtaButton = ({ variant = 'filled', ...rest }: Props) => { - const { palettes, colors, fontSizes, spacing, dark } = useTheme(); + const { palettes, colors, fontSizes, spacing, dark, fontWeights } = + useTheme(); const styles = useStylesheet(createStyles); const { left, right } = useSafeAreaInsets(); const bottomBarHeight = useSafeBottomBarHeight(); @@ -159,7 +161,7 @@ export const CtaButton = ({ style={{ marginRight: spacing[2] }} /> )} - {title} - + {rightExtra && rightExtra} @@ -190,13 +193,7 @@ export const CtaButtonSpacer = () => { return ; }; -const createStyles = ({ - colors, - shapes, - spacing, - fontSizes, - fontWeights, -}: Theme) => +const createStyles = ({ colors, shapes, spacing, fontSizes }: Theme) => StyleSheet.create({ container: { padding: spacing[4], @@ -222,7 +219,6 @@ const createStyles = ({ }, textStyle: { fontSize: fontSizes.md, - fontWeight: fontWeights.medium, textAlign: 'center', color: colors.white, }, diff --git a/lib/ui/components/CtaButtonContainer.tsx b/lib/ui/components/CtaButtonContainer.tsx index 76dd28c8..12b531cd 100644 --- a/lib/ui/components/CtaButtonContainer.tsx +++ b/lib/ui/components/CtaButtonContainer.tsx @@ -35,6 +35,7 @@ export const CtaButtonContainer = ({ }, absolute && { position: 'absolute', + width: Platform.select({ android: '100%' }), left: Platform.select({ ios: left }), right, bottom: diff --git a/src/core/components/TextWithLinks.tsx b/src/core/components/TextWithLinks.tsx index eb32908c..91f227a0 100644 --- a/src/core/components/TextWithLinks.tsx +++ b/src/core/components/TextWithLinks.tsx @@ -1,20 +1,21 @@ import { PropsWithChildren } from 'react'; import { TextProps } from 'react-native'; +import { MixedStyleDeclaration } from 'react-native-render-html'; import { linkUrls } from '../../utils/html'; import { HtmlView } from './HtmlView'; -export const TextWithLinks = ({ - children, - style, -}: PropsWithChildren) => { - if (!children || typeof children !== 'string') return null; +type Props = { + baseStyle?: MixedStyleDeclaration; +} & PropsWithChildren; +export const TextWithLinks = ({ baseStyle, children, style }: Props) => { + if (!children || typeof children !== 'string') return null; const html = linkUrls(children); return ( ); diff --git a/src/core/queries/authHooks.ts b/src/core/queries/authHooks.ts index fe14d4db..2f06fc16 100644 --- a/src/core/queries/authHooks.ts +++ b/src/core/queries/authHooks.ts @@ -24,7 +24,7 @@ export const useLogin = () => { return useMutation({ mutationFn: (dto: LoginRequest) => { - const client = { name: 'Students app' }; + const client = { name: 'Students app', id: 'students-app' }; return Promise.all([ DeviceInfo.getDeviceName(), diff --git a/src/features/transcript/components/ProvisionalGradeListItem.tsx b/src/features/transcript/components/ProvisionalGradeListItem.tsx index 812244fd..fb88e922 100644 --- a/src/features/transcript/components/ProvisionalGradeListItem.tsx +++ b/src/features/transcript/components/ProvisionalGradeListItem.tsx @@ -11,6 +11,7 @@ import { ProvisionalGradeStateEnum, } from '@polito/api-client/models/ProvisionalGrade'; +import { TextWithLinks } from '../../../core/components/TextWithLinks'; import { IS_IOS } from '../../../core/constants'; import { formatDate, formatTime } from '../../../utils/dates'; import { useGetRejectionTime } from '../hooks/useGetRejectionTime'; @@ -34,7 +35,16 @@ export const ProvisionalGradeListItem = ({ grade }: Props) => { const subtitle = useMemo(() => { switch (grade.state) { case ProvisionalGradeStateEnum.Confirmed: - return rejectionTime; + if (grade.canBeRejected) { + return ( + + {t('transcriptGradesScreen.rejectionCountdown', { + hours: rejectionTime, + })} + + ); + } + break; case ProvisionalGradeStateEnum.Rejected: return t('transcriptGradesScreen.rejectedSubtitle', { date: formatDate(grade.rejectedAt!), @@ -73,11 +83,12 @@ export const ProvisionalGradeListItem = ({ grade }: Props) => { ); }; -const createStyles = ({ colors, dark, palettes }: Theme) => ({ +const createStyles = ({ colors, dark, palettes, fontSizes }: Theme) => ({ subtitle: { color: colors.title, }, rejectableSubtitle: { + fontSize: fontSizes.sm, color: dark ? palettes.danger[300] : palettes.danger[700], }, }); diff --git a/src/features/transcript/hooks/useGetRejectionTime.tsx b/src/features/transcript/hooks/useGetRejectionTime.tsx index c421762a..bc25d7c4 100644 --- a/src/features/transcript/hooks/useGetRejectionTime.tsx +++ b/src/features/transcript/hooks/useGetRejectionTime.tsx @@ -21,18 +21,18 @@ export const useGetRejectionTime = ({ return t('transcriptGradesScreen.expiredCountdown'); } - let time = t('transcriptGradesScreen.rejectionCountdown'); + let time = ''; const hours = Math.floor(diff / (1000 * 60 * 60)); // count hours if (hours > 0) { - time += ` ${hours} ${t('common.hours').toLowerCase()}`; + time += ` ${hours}h`; } if (hours === 0 || !isCompact) { const minutes = Math.floor((diff / (1000 * 60)) % 60); - time += ` ${minutes} ${t('common.minutes').toLowerCase()}`; + time += ` ${minutes}m`; } return time; diff --git a/src/features/transcript/screens/ProvisionalGradeScreen.tsx b/src/features/transcript/screens/ProvisionalGradeScreen.tsx index 3c7f137b..de5df371 100644 --- a/src/features/transcript/screens/ProvisionalGradeScreen.tsx +++ b/src/features/transcript/screens/ProvisionalGradeScreen.tsx @@ -1,6 +1,6 @@ import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { SafeAreaView, ScrollView, StyleSheet } from 'react-native'; +import { Platform, SafeAreaView, ScrollView, StyleSheet } from 'react-native'; import { ActivityIndicator } from '@lib/ui/components/ActivityIndicator'; import { Col } from '@lib/ui/components/Col'; @@ -11,6 +11,7 @@ import { Row } from '@lib/ui/components/Row'; import { ScreenTitle } from '@lib/ui/components/ScreenTitle'; 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 { ProvisionalGradeStateEnum } from '@polito/api-client/models/ProvisionalGrade'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; @@ -35,6 +36,7 @@ export const ProvisionalGradeScreen = ({ navigation, route }: Props) => { const { t } = useTranslation(); const styles = useStylesheet(createStyles); const { setFeedback } = useFeedbackContext(); + const { fontWeights } = useTheme(); const confirmAcceptance = useConfirmationDialog({ title: t('common.areYouSure?'), @@ -91,7 +93,18 @@ export const ProvisionalGradeScreen = ({ navigation, route }: Props) => { ) : ( - + {`${formatDate(grade.date)} - ${t( @@ -100,10 +113,6 @@ export const ProvisionalGradeScreen = ({ navigation, route }: Props) => { credits: grade.credits, }, )}`} - {grade.state === ProvisionalGradeStateEnum.Confirmed && - rejectionTime && ( - {rejectionTime} - )} { style={styles.grade} > - {grade.grade}{' '} + {grade.isFailure || grade.isWithdrawn + ? grade.grade.charAt(0).toUpperCase() + + grade.grade.slice(1).toLowerCase() + : grade.grade} + {grade.state === ProvisionalGradeStateEnum.Confirmed && + grade.canBeAccepted && + rejectionTime && ( + + + {t('transcriptGradesScreen.autoRegistration')} + + {rejectionTime} + + + + )} + {grade?.state === ProvisionalGradeStateEnum.Confirmed && ( @@ -140,54 +173,63 @@ export const ProvisionalGradeScreen = ({ navigation, route }: Props) => { /> )} {grade?.state === ProvisionalGradeStateEnum.Confirmed && ( - - - confirmAcceptance().then(ok => { - if (ok) { - acceptGradeQuery - .mutateAsync(grade.id) - .then(() => provideFeedback(true)); - } - }) - } - variant="outlined" - absolute={false} - loading={acceptGradeQuery.isLoading} - disabled={ - isOffline || - acceptGradeQuery.isLoading || - rejectGradeQuery.isLoading - } - containerStyle={{ paddingVertical: 0 }} - /> - - confirmRejection().then(ok => { - if (ok) { - rejectGradeQuery - .mutateAsync(grade.id) - .then(() => provideFeedback(false)); - } - }) - } - absolute={false} - loading={rejectGradeQuery.isLoading} - disabled={ - isOffline || - acceptGradeQuery.isLoading || - rejectGradeQuery.isLoading - } - containerStyle={{ paddingVertical: 0 }} - /> + + {grade?.canBeAccepted && ( + + confirmAcceptance().then(ok => { + if (ok) { + acceptGradeQuery + .mutateAsync(grade.id) + .then(() => provideFeedback(true)); + } + }) + } + absolute={false} + loading={acceptGradeQuery.isLoading} + disabled={ + isOffline || + acceptGradeQuery.isLoading || + rejectGradeQuery.isLoading + } + containerStyle={{ paddingVertical: 0 }} + /> + )} + {grade?.canBeRejected && ( + + confirmRejection().then(ok => { + if (ok) { + rejectGradeQuery + .mutateAsync(grade.id) + .then(() => provideFeedback(false)); + } + }) + } + absolute={false} + loading={rejectGradeQuery.isLoading} + variant="outlined" + disabled={ + isOffline || + acceptGradeQuery.isLoading || + rejectGradeQuery.isLoading + } + containerStyle={{ paddingVertical: 0 }} + destructive + /> + )} )} ); }; - const createStyles = ({ colors, dark, @@ -228,10 +270,17 @@ const createStyles = ({ fontWeight: fontWeights.semibold, }, longGradeText: { - fontSize: fontSizes.md, + fontSize: fontSizes.lg, fontWeight: fontWeights.semibold, }, + failureGradeText: { + color: palettes.rose[600], + }, rejectionTime: { color: dark ? palettes.danger[300] : palettes.danger[700], }, + autoRegistration: { + fontSize: fontSizes.md, + color: dark ? palettes.primary[300] : palettes.primary[600], + }, }); From e1f682257c7e208edb163fd3a9874ff42ca8d389 Mon Sep 17 00:00:00 2001 From: Fabrizio Costa Medich <134924838+FabrizioCostaMedich@users.noreply.github.com> Date: Mon, 17 Jun 2024 21:27:25 +0200 Subject: [PATCH 3/8] fix(exams): provide a feedback when an exam cannot be booked, Ref #495 (#499) * fix(exams): provide a feedback when an exam cannot be booked, Ref #495 * fix(exams): fix icon not bookable and places not available, Refs #495 --- assets/translations/en.json | 1 + assets/translations/it.json | 1 + lib/ui/components/CtaButton.tsx | 1 + lib/ui/components/ErrorCard.tsx | 41 +++++++++++++++++++ lib/ui/types/Theme.ts | 3 ++ package-lock.json | 8 ++-- package.json | 2 +- src/core/themes/dark.ts | 3 ++ src/core/themes/light.ts | 3 ++ src/features/teaching/components/ExamCTA.tsx | 9 ++-- src/features/teaching/screens/ExamScreen.tsx | 43 +++++++++++++++++++- src/features/teaching/utils/exam.ts | 12 ++++++ 12 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 lib/ui/components/ErrorCard.tsx create mode 100644 src/features/teaching/utils/exam.ts diff --git a/assets/translations/en.json b/assets/translations/en.json index 1f01a327..973cb97c 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -424,6 +424,7 @@ "location": "Location", "noClassroom": "No place specified for this exam call", "noLocation": "Room to be defined", + "notAvailable": "Booking not available", "notes": "Notes", "title": "Exam call" }, diff --git a/assets/translations/it.json b/assets/translations/it.json index 8da237c1..0f78de2b 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -424,6 +424,7 @@ "location": "Luogo", "noClassroom": "Nessun luogo specificato per questo appello", "noLocation": "Aula da definire", + "notAvailable": "Prenotazione non disponibile", "notes": "Note", "title": "Appello" }, diff --git a/lib/ui/components/CtaButton.tsx b/lib/ui/components/CtaButton.tsx index 1426b1e5..938997b8 100644 --- a/lib/ui/components/CtaButton.tsx +++ b/lib/ui/components/CtaButton.tsx @@ -170,6 +170,7 @@ export const CtaButton = ({ { color: variant === 'filled' ? colors.white : color, }, + disabled ? { color: colors.disableTitle } : undefined, ]} baseStyle={{ fontWeight: fontWeights.medium }} > diff --git a/lib/ui/components/ErrorCard.tsx b/lib/ui/components/ErrorCard.tsx new file mode 100644 index 00000000..b0f0c654 --- /dev/null +++ b/lib/ui/components/ErrorCard.tsx @@ -0,0 +1,41 @@ +import { PropsWithChildren } from 'react'; +import { Platform, ViewProps } from 'react-native'; + +import { Card } from '@lib/ui/components/Card'; +import { Text } from '@lib/ui/components/Text'; +import { useTheme } from '@lib/ui/hooks/useTheme'; + +type Props = PropsWithChildren< + ViewProps & { + text: string; + } +>; + +export const ErrorCard = ({ text, ...rest }: Props) => { + const { spacing, fontSizes, colors } = useTheme(); + return ( + + + {text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()} + + + ); +}; diff --git a/lib/ui/types/Theme.ts b/lib/ui/types/Theme.ts index 9caf8001..7e984a47 100644 --- a/lib/ui/types/Theme.ts +++ b/lib/ui/types/Theme.ts @@ -83,6 +83,7 @@ export interface Colors { heading: string; subHeading: string; prose: string; + disableTitle: string; longProse: string; secondaryText: string; caption: string; @@ -96,6 +97,8 @@ export interface Colors { deadlineCardBorder: string; examCardBorder: string; lectureCardSecondary: string; + errorCardText: string; + errorCardBorder: string; translucentSurface: string; white: string; } diff --git a/package-lock.json b/package-lock.json index faf5e613..da86b3c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@miblanchard/react-native-slider": "^2.2.0", "@openspacelabs/react-native-zoomable-view": "^2.1.5", "@orama/orama": "^2.0.0-beta.8", - "@polito/api-client": "^1.0.0-ALPHA.63", + "@polito/api-client": "^1.0.0-ALPHA.64", "@react-native-async-storage/async-storage": "^1.18.2", "@react-native-clipboard/clipboard": "^1.12.1", "@react-native-community/blur": "^4.3.0", @@ -4017,9 +4017,9 @@ } }, "node_modules/@polito/api-client": { - "version": "1.0.0-ALPHA.63", - "resolved": "https://npm.pkg.github.com/download/@polito/api-client/1.0.0-ALPHA.63/c061fbc916fd4dfdca7edec5a2ef838593cbc22a", - "integrity": "sha512-AGnKq41yX1MBQOqztG+1cQu2OhfTZ0tKGMr9b8ez8tbual1axW94QLwu/ECmQM/7258sqcA/H8GuTgI+6UEPCw==" + "version": "1.0.0-ALPHA.64", + "resolved": "https://npm.pkg.github.com/download/@polito/api-client/1.0.0-ALPHA.64/3f3da2fd8e53974247d9116582d442709ed0332b", + "integrity": "sha512-DUywgHp7R10jYFukfuzu7HvJvnWNFzAgBM+VxIIfz6Dto3YfrjknSEpNF3EjL5xSLlpBivKUAgxLiKiJn7G4Zw==" }, "node_modules/@react-native-async-storage/async-storage": { "version": "1.19.8", diff --git a/package.json b/package.json index 5c7b8834..90ee74a5 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@miblanchard/react-native-slider": "^2.2.0", "@openspacelabs/react-native-zoomable-view": "^2.1.5", "@orama/orama": "^2.0.0-beta.8", - "@polito/api-client": "^1.0.0-ALPHA.63", + "@polito/api-client": "^1.0.0-ALPHA.64", "@react-native-async-storage/async-storage": "^1.18.2", "@react-native-clipboard/clipboard": "^1.12.1", "@react-native-community/blur": "^4.3.0", diff --git a/src/core/themes/dark.ts b/src/core/themes/dark.ts index 5555ac4e..d7519cb7 100644 --- a/src/core/themes/dark.ts +++ b/src/core/themes/dark.ts @@ -19,6 +19,7 @@ export const darkTheme: Theme = { subHeading: lightTheme.palettes.info[400], title: 'white', prose: lightTheme.palettes.text[50], + disableTitle: lightTheme.palettes.gray[700], longProse: lightTheme.palettes.text[50], secondaryText: lightTheme.palettes.text[400], caption: lightTheme.palettes.text[500], @@ -31,5 +32,7 @@ export const darkTheme: Theme = { touchableHighlight: 'rgba(255, 255, 255, .08)', lectureCardSecondary: lightTheme.palettes.gray[300], tabBarInactive: lightTheme.palettes.gray[400], + errorCardText: lightTheme.palettes.rose[200], + errorCardBorder: lightTheme.palettes.rose[500], }, }; diff --git a/src/core/themes/light.ts b/src/core/themes/light.ts index 9836421d..b2cc1ed2 100644 --- a/src/core/themes/light.ts +++ b/src/core/themes/light.ts @@ -150,6 +150,7 @@ export const lightTheme: Theme = { subHeading: lightBlue[700], title: navy[700], prose: gray[800], + disableTitle: '#FFFFFF', longProse: gray[800], secondaryText: gray[500], caption: gray[500], @@ -162,6 +163,8 @@ export const lightTheme: Theme = { deadlineCardBorder: red[700], examCardBorder: orange[600], lectureCardSecondary: gray[600], + errorCardText: rose[700], + errorCardBorder: rose[500], }, palettes: { navy, diff --git a/src/features/teaching/components/ExamCTA.tsx b/src/features/teaching/components/ExamCTA.tsx index 548e5e55..597a6a39 100644 --- a/src/features/teaching/components/ExamCTA.tsx +++ b/src/features/teaching/components/ExamCTA.tsx @@ -32,13 +32,13 @@ export const ExamCTA = ({ exam }: Props) => { const examRequestable = exam?.status === ExamStatusEnum.Requestable; const examAvailable = exam?.status === ExamStatusEnum.Available; + const examUnavailable = exam?.status === ExamStatusEnum.Unavailable; const confirm = useConfirmationDialog(); const disabledStatuses = [ ExamStatusEnum.RequestAccepted, ExamStatusEnum.RequestRejected, - ExamStatusEnum.Unavailable, ] as ExamStatusEnum[]; const action = async () => { if (examRequestable) { @@ -80,7 +80,9 @@ export const ExamCTA = ({ exam }: Props) => { { } action={action} loading={mutationsLoading} - disabled={!onlineManager.isOnline()} + disabled={!onlineManager.isOnline() || examUnavailable} + variant="filled" /> ); }; diff --git a/src/features/teaching/screens/ExamScreen.tsx b/src/features/teaching/screens/ExamScreen.tsx index adf3ed5c..a00a0dd1 100644 --- a/src/features/teaching/screens/ExamScreen.tsx +++ b/src/features/teaching/screens/ExamScreen.tsx @@ -3,9 +3,14 @@ import { useTranslation } from 'react-i18next'; import { SafeAreaView, ScrollView, View } from 'react-native'; import { faNoteSticky } from '@fortawesome/free-regular-svg-icons'; -import { faHourglassEnd, faUsers } from '@fortawesome/free-solid-svg-icons'; +import { + faHourglassEnd, + faTriangleExclamation, + faUsers, +} from '@fortawesome/free-solid-svg-icons'; import { Col } from '@lib/ui/components/Col'; import { CtaButtonSpacer } from '@lib/ui/components/CtaButton'; +import { ErrorCard } from '@lib/ui/components/ErrorCard'; import { Icon } from '@lib/ui/components/Icon'; import { ListItem } from '@lib/ui/components/ListItem'; import { OverviewList } from '@lib/ui/components/OverviewList'; @@ -16,6 +21,7 @@ import { ScreenDateTime } from '@lib/ui/components/ScreenDateTime'; import { ScreenTitle } from '@lib/ui/components/ScreenTitle'; import { Text } from '@lib/ui/components/Text'; import { useTheme } from '@lib/ui/hooks/useTheme'; +import { ExamStatusEnum } from '@polito/api-client'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { BottomBarSpacer } from '../../../core/components/BottomBarSpacer'; @@ -36,6 +42,7 @@ import { ExamCpdModalContent } from '../../surveys/components/ExamCpdModalConten import { ExamCTA } from '../components/ExamCTA'; import { ExamStatusBadge } from '../components/ExamStatusBadge'; import { TeachingStackParamList } from '../components/TeachingNavigator'; +import { getExam, isExamPassed } from '../utils/exam'; type Props = NativeStackScreenProps; @@ -187,14 +194,46 @@ export const ExamScreen = ({ route, navigation }: Props) => { : t('common.dateToBeDefined') } subtitle={t('examScreen.bookingEndsAt')} + trailingItem={ + exam?.status === ExamStatusEnum.Unavailable && + exam?.bookingEndsAt && + isExamPassed(exam.bookingEndsAt) ? ( + + ) : undefined + } /> + } inverted - title={`${exam?.bookedCount}`} + /* check using undefined since the fields can be 0 */ + title={ + exam?.availableCount !== undefined && + exam?.bookedCount !== undefined + ? getExam(exam.bookedCount, exam.availableCount) + : '' + } subtitle={t('examScreen.bookedCount')} + trailingItem={ + exam?.status === ExamStatusEnum.Unavailable && + exam?.availableCount && + exam.availableCount === 0 ? ( + + ) : undefined + } /> + {exam?.feedback && exam?.status === ExamStatusEnum.Unavailable && ( + + )} diff --git a/src/features/teaching/utils/exam.ts b/src/features/teaching/utils/exam.ts new file mode 100644 index 00000000..3b29888a --- /dev/null +++ b/src/features/teaching/utils/exam.ts @@ -0,0 +1,12 @@ +import { DateTime } from 'luxon'; + +export const isExamPassed = (bookingEndsAt: Date) => { + return DateTime.now().setZone('Europe/Rome').toJSDate() > bookingEndsAt; +}; + +export const getExam = (bookedCount: number, availableCount: number) => { + if (availableCount === 999) { + return `${bookedCount}`; + } + return `${bookedCount}/${availableCount + bookedCount}`; +}; From a5ab7a6940be10be6fc81696b3293e2e2f1580fd Mon Sep 17 00:00:00 2001 From: Umberto Pepato Date: Mon, 17 Jun 2024 21:54:19 +0200 Subject: [PATCH 4/8] fix(teaching): update file name failing when already scheduled (#496) --- ios/Podfile.lock | 2 +- package-lock.json | 7 ++++--- package.json | 2 +- src/core/hooks/useDownloadCourseFile.ts | 12 ++++++++---- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6777661e..9b3918db 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1020,7 +1020,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - boost: 7dcd2de282d72e344012f7d6564d024930a6a440 + boost: 57d2868c099736d80fcd648bf211b4431e51a558 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: d5c36294933aa344046699700b9ae9c2e10db18e diff --git a/package-lock.json b/package-lock.json index da86b3c7..e023f3a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -125,7 +125,7 @@ "jest": "^29.2.1", "lint-staged": "^13.0.3", "metro-react-native-babel-preset": "0.76.9", - "pod-install": "0.1.38", + "pod-install": "^0.2.2", "prettier": "^2.7.1", "react-test-renderer": "18.2.0", "standard-version": "^9.5.0", @@ -15837,9 +15837,10 @@ } }, "node_modules/pod-install": { - "version": "0.1.38", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/pod-install/-/pod-install-0.2.2.tgz", + "integrity": "sha512-NgQpKiuWZo8mWU+SVxmrn+ARy9+fFYzW53ze6CDTo70u5Ie8AVSn7FqolDC/c7+N4/kQ1BldAnXEab6SNYA8xw==", "dev": true, - "license": "MIT", "bin": { "pod-install": "build/index.js" } diff --git a/package.json b/package.json index 90ee74a5..22d58533 100644 --- a/package.json +++ b/package.json @@ -135,7 +135,7 @@ "jest": "^29.2.1", "lint-staged": "^13.0.3", "metro-react-native-babel-preset": "0.76.9", - "pod-install": "0.1.38", + "pod-install": "^0.2.2", "prettier": "^2.7.1", "react-test-renderer": "18.2.0", "standard-version": "^9.5.0", diff --git a/src/core/hooks/useDownloadCourseFile.ts b/src/core/hooks/useDownloadCourseFile.ts index 73319455..be4fda6f 100644 --- a/src/core/hooks/useDownloadCourseFile.ts +++ b/src/core/hooks/useDownloadCourseFile.ts @@ -68,10 +68,14 @@ export const useDownloadCourseFile = ( updateDownload({ isDownloaded: true }); } else { // Update the name when changed - await mkdir(dirname(toFile)); - await moveFile(cachedFilePath, toFile); - await cleanupEmptyFolders(coursesFilesCachePath); - refresh(); + try { + await mkdir(dirname(toFile)); + await moveFile(cachedFilePath, toFile); + await cleanupEmptyFolders(coursesFilesCachePath); + refresh(); + } catch (_) { + // File rename was already scheduled + } } } else { updateDownload({ isDownloaded: false }); From 3b32377f68321718df4b2d5f861703a0fb5a16f4 Mon Sep 17 00:00:00 2001 From: Cristina Ferrian Date: Mon, 17 Jun 2024 22:17:31 +0200 Subject: [PATCH 5/8] build: bump version --- ios/Podfile.lock | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9b3918db..6777661e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1020,7 +1020,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - boost: 57d2868c099736d80fcd648bf211b4431e51a558 + boost: 7dcd2de282d72e344012f7d6564d024930a6a440 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: d5c36294933aa344046699700b9ae9c2e10db18e diff --git a/package-lock.json b/package-lock.json index e023f3a5..80dadfa2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@polito/students-app", - "version": "1.6.5", + "version": "1.6.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@polito/students-app", - "version": "1.6.5", + "version": "1.6.6", "hasInstallScript": true, "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.2.1", diff --git a/package.json b/package.json index 22d58533..98a6b8d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@polito/students-app", - "version": "1.6.5", + "version": "1.6.6", "private": true, "scripts": { "android": "react-native run-android --active-arch-only --appIdSuffix=dev", From 7c84dd0269c560490277248b572ea6d8ca09d898 Mon Sep 17 00:00:00 2001 From: Fabrizio Costa Medich <134924838+FabrizioCostaMedich@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:11:34 +0200 Subject: [PATCH 6/8] fix(exams): sort exams by date in the TeachingScreen.tsx, Ref #504 (#505) --- .../teaching/screens/TeachingScreen.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/features/teaching/screens/TeachingScreen.tsx b/src/features/teaching/screens/TeachingScreen.tsx index ea65ffb1..8fc3f17c 100644 --- a/src/features/teaching/screens/TeachingScreen.tsx +++ b/src/features/teaching/screens/TeachingScreen.tsx @@ -28,6 +28,8 @@ import { Theme } from '@lib/ui/types/Theme'; import { ExamStatusEnum } from '@polito/api-client'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { DateTime } from 'luxon'; + import { BottomBarSpacer } from '../../../core/components/BottomBarSpacer'; import { usePreferencesContext } from '../../../core/contexts/PreferencesContext'; import { useNotifications } from '../../../core/hooks/useNotifications'; @@ -83,8 +85,19 @@ export const TeachingScreen = ({ navigation }: Props) => { return ( examsQuery.data - .filter(e => !hiddenCourses.includes(e.uniqueShortcode)) - .sort(e => (e.status === ExamStatusEnum.Booked ? -1 : 1)) + .filter( + e => + !hiddenCourses.includes(e.uniqueShortcode) && + e.examEndsAt!.valueOf() > DateTime.now().toJSDate().valueOf(), + ) + .sort((a, b) => { + const status = + (a.status === ExamStatusEnum.Booked ? -1 : 0) + + (b.status === ExamStatusEnum.Booked ? 1 : 0); + return status !== 0 + ? status + : a.examStartsAt!.valueOf() - b.examStartsAt!.valueOf(); + }) .slice(0, 4) ?? [] ); }, [coursePreferences, coursesQuery.data, examsQuery.data]); From 7db8d40710a7a0839c36a0cbb1050f6b97209873 Mon Sep 17 00:00:00 2001 From: Federico Cucinella <10742159+QcFe@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:52:30 +0200 Subject: [PATCH 7/8] fix(login): enable login even when fcm is not enabled, but display warning (#515) fixes: #513 --- assets/translations/en.json | 3 ++- assets/translations/it.json | 3 ++- src/core/queries/authHooks.ts | 18 ++++++++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 973cb97c..3f76c7d3 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -490,7 +490,8 @@ "title": "Log in with your polito.it credentials", "unsupportedUserType": "User type not supported: only students can log in to PoliTO Students.", "usernameLabel": "Student ID", - "usernameLabelAccessibility": "Enter your username here" + "usernameLabelAccessibility": "Enter your username here", + "fcmUnsupported": "An error occurred during push notifications setup. This problem can be caused by the lack of Google Play Services on this device. It is possible to keep using this app, however push notifications won't be received." }, "messageScreen": { "backTitle": "Archive", diff --git a/assets/translations/it.json b/assets/translations/it.json index 0f78de2b..0e988c3f 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -490,7 +490,8 @@ "title": "Accedi con le tue credenziali polito.it", "unsupportedUserType": "Tipo utente non supportato: PoliTO Students è accessibile solamente agli studenti.", "usernameLabel": "Matricola", - "usernameLabelAccessibility": "Inserisci qui il tuo username" + "usernameLabelAccessibility": "Inserisci qui il tuo username", + "fcmUnsupported": "Si è verificato un errore durante l'attivazione delle notifiche push. Questo problema può essere causato dall'assenza dei servizi Google su questo dispositivo. È possibile continuare ad utilizzare l'app ma non si riceveranno notifiche push." }, "messageScreen": { "backTitle": "Archivio", diff --git a/src/core/queries/authHooks.ts b/src/core/queries/authHooks.ts index 2f06fc16..5c8627fc 100644 --- a/src/core/queries/authHooks.ts +++ b/src/core/queries/authHooks.ts @@ -1,4 +1,4 @@ -import { Platform } from 'react-native'; +import { Alert, Platform } from 'react-native'; import DeviceInfo from 'react-native-device-info'; import Keychain from 'react-native-keychain'; @@ -6,6 +6,8 @@ import { AuthApi, LoginRequest, SwitchCareerRequest } from '@polito/api-client'; import messaging from '@react-native-firebase/messaging'; import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { t } from 'i18next'; + import { isEnvProduction } from '../../utils/env'; import { pluckData } from '../../utils/queries'; import { useApiContext } from '../contexts/ApiContext'; @@ -17,6 +19,18 @@ const useAuthClient = (): AuthApi => { return new AuthApi(); }; +async function getFcmToken(): Promise { + if (!isEnvProduction) return undefined; + + try { + return await messaging().getToken(); + } catch (_) { + Alert.alert(t('common.error'), t('loginScreen.fcmUnsupported')); + } + + return undefined; +} + export const useLogin = () => { const authClient = useAuthClient(); const { refreshContext } = useApiContext(); @@ -32,7 +46,7 @@ export const useLogin = () => { DeviceInfo.getManufacturer(), DeviceInfo.getBuildNumber(), DeviceInfo.getVersion(), - isEnvProduction ? messaging().getToken() : undefined, + getFcmToken(), ]) .then( ([ From 3ac5546efe942403934fa34610dcd9b66ea546c2 Mon Sep 17 00:00:00 2001 From: Fabrizio Costa Medich <134924838+FabrizioCostaMedich@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:43:21 +0200 Subject: [PATCH 8/8] fix(exams): fix exam booked count and available count (#511) --- assets/translations/en.json | 3 ++- assets/translations/it.json | 3 ++- src/features/teaching/screens/ExamScreen.tsx | 4 +--- src/features/teaching/utils/exam.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 3f76c7d3..6849ee51 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -426,7 +426,8 @@ "noLocation": "Room to be defined", "notAvailable": "Booking not available", "notes": "Notes", - "title": "Exam call" + "title": "Exam call", + "noBookedCount": "Data not available" }, "examsScreen": { "emptyState": "There are no open exam calls", diff --git a/assets/translations/it.json b/assets/translations/it.json index 0e988c3f..83180785 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -426,7 +426,8 @@ "noLocation": "Aula da definire", "notAvailable": "Prenotazione non disponibile", "notes": "Note", - "title": "Appello" + "title": "Appello", + "noBookedCount": "Dato non disponibile" }, "examsScreen": { "emptyState": "Non ci sono appelli disponibili", diff --git a/src/features/teaching/screens/ExamScreen.tsx b/src/features/teaching/screens/ExamScreen.tsx index a00a0dd1..37795dc9 100644 --- a/src/features/teaching/screens/ExamScreen.tsx +++ b/src/features/teaching/screens/ExamScreen.tsx @@ -212,15 +212,13 @@ export const ExamScreen = ({ route, navigation }: Props) => { inverted /* check using undefined since the fields can be 0 */ title={ - exam?.availableCount !== undefined && exam?.bookedCount !== undefined ? getExam(exam.bookedCount, exam.availableCount) - : '' + : t('examScreen.noBookedCount') } subtitle={t('examScreen.bookedCount')} trailingItem={ exam?.status === ExamStatusEnum.Unavailable && - exam?.availableCount && exam.availableCount === 0 ? ( { }; export const getExam = (bookedCount: number, availableCount: number) => { - if (availableCount === 999) { + if (availableCount === undefined || availableCount === 999) { return `${bookedCount}`; } return `${bookedCount}/${availableCount + bookedCount}`;