From 35c5d5dc85624bf00f873e6b9ce21410f1a826ad Mon Sep 17 00:00:00 2001 From: Graham Blair Date: Sat, 26 Oct 2024 17:06:38 -0700 Subject: [PATCH 1/3] Start work on responsive colors for QR code scans --- .../AttendeesCheckInTable.module.scss | 68 ------ .../QrScanner.module.scss | 11 + .../common/AttendeeCheckInTable/QrScanner.tsx | 26 ++- .../common/AttendeeCheckInTable/index.tsx | 204 ------------------ .../src/components/layouts/CheckIn/index.tsx | 4 +- 5 files changed, 36 insertions(+), 277 deletions(-) delete mode 100644 frontend/src/components/common/AttendeeCheckInTable/AttendeesCheckInTable.module.scss delete mode 100644 frontend/src/components/common/AttendeeCheckInTable/index.tsx diff --git a/frontend/src/components/common/AttendeeCheckInTable/AttendeesCheckInTable.module.scss b/frontend/src/components/common/AttendeeCheckInTable/AttendeesCheckInTable.module.scss deleted file mode 100644 index 53d96cdbe..000000000 --- a/frontend/src/components/common/AttendeeCheckInTable/AttendeesCheckInTable.module.scss +++ /dev/null @@ -1,68 +0,0 @@ -@import '../../../styles/mixins'; - -.header { - position: sticky; - top: 0; - z-index: 2; - display: flex; - flex-direction: column; - - .checkInCount { - font-size: 0.7em; - color: #999; - } - - .search { - flex: 1; - - .searchBar { - display: flex; - gap: 20px; - align-items: center; - - .searchInput { - flex: 1; - margin-bottom: 0 !important; - } - - .scanButton { - @include respond-below(sm) { - display: none; - } - } - - .scanIcon { - display: none; - @include respond-below(sm) { - display: flex; - } - } - } - } - - .stats { - flex: 1; - } -} - -.loading, .noResults { - display: flex; - justify-content: center; - align-items: center; - margin-top: 50px; -} - -.attendees { - .attendee { - display: flex; - align-items: center; - - .details { - flex: 1; - } - - .actions { - flex-grow: initial; - } - } -} diff --git a/frontend/src/components/common/AttendeeCheckInTable/QrScanner.module.scss b/frontend/src/components/common/AttendeeCheckInTable/QrScanner.module.scss index c22c2e81b..189811d67 100644 --- a/frontend/src/components/common/AttendeeCheckInTable/QrScanner.module.scss +++ b/frontend/src/components/common/AttendeeCheckInTable/QrScanner.module.scss @@ -63,6 +63,7 @@ animation: colorfulBorder 10s infinite; border-radius: 10px; outline: solid 50vmax #472e7840; + transition: outline-color .2s ease-out; min-width: 200px; min-height: 200px; @@ -72,6 +73,16 @@ } } + .scannerOverlay.success { + outline: solid 50vmax rgb(72 193 72 / 50%); + + } + + .scannerOverlay.failure { + outline: solid 50vmax rgb(193 72 72 / 50%); + + } + video { width: 100vw !important; height: 100vh !important; diff --git a/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx b/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx index c90465173..a5251035e 100644 --- a/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx +++ b/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx @@ -8,7 +8,7 @@ import {showError} from "../../../utilites/notifications.tsx"; import {t, Trans} from "@lingui/macro"; interface QRScannerComponentProps { - onCheckIn: (attendeePublicId: string, onRequestComplete: () => void, onFailure: () => void) => void; + onCheckIn: (attendeePublicId: string, onRequestComplete: (didSucceed: bool) => void, onFailure: () => void) => void; onClose: () => void; } @@ -26,6 +26,8 @@ export const QRScannerComponent = (props: QRScannerComponentProps) => { const [currentAttendeeId, setCurrentAttendeeId] = useState(null); const [debouncedAttendeeId] = useDebouncedValue(currentAttendeeId, 500); + const [isScanFailed, setIsScanFailed] = useState(false); + const [isScanSucceeded, setIsScanSucceeded] = useState(false); useEffect(() => { latestProcessedAttendeeIdsRef.current = processedAttendeeIds; @@ -56,15 +58,33 @@ export const QRScannerComponent = (props: QRScannerComponentProps) => { if (alreadyScanned) { showError(t`You already scanned this ticket`); + setIsScanFailed(true); + setInterval(function() { + setIsScanFailed(false); + }, 1000); return; } if (!isCheckingIn && !alreadyScanned) { setIsCheckingIn(true); - props.onCheckIn(debouncedAttendeeId, () => { + props.onCheckIn(debouncedAttendeeId, (didSucceed) => { setIsCheckingIn(false); setProcessedAttendeeIds(prevIds => [...prevIds, debouncedAttendeeId]); setCurrentAttendeeId(null); + + console.log(`Did it succeed? ${didSucceed}`) + console.log(this); + if (didSucceed) { + setIsScanSucceeded(true); + setInterval(function() { + setIsScanSucceeded(false); + }, 1000); + } else { + setIsScanFailed(true); + setInterval(function() { + setIsScanFailed(false); + }, 1000); + } }, () => { setIsCheckingIn(false); setCurrentAttendeeId(null); @@ -178,7 +198,7 @@ export const QRScannerComponent = (props: QRScannerComponentProps) => { -
+
); }; diff --git a/frontend/src/components/common/AttendeeCheckInTable/index.tsx b/frontend/src/components/common/AttendeeCheckInTable/index.tsx deleted file mode 100644 index 37f5a004e..000000000 --- a/frontend/src/components/common/AttendeeCheckInTable/index.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import {ActionIcon, Button, Loader, Modal, Switch} from "@mantine/core"; -import classes from './AttendeesCheckInTable.module.scss'; -import {useParams} from "react-router-dom"; -import {Card} from "../Card"; -import {PageTitle} from "../PageTitle"; -import {SearchBar} from "../SearchBar"; -import {useState} from "react"; -import {useDebouncedValue} from "@mantine/hooks"; -import {useGetAttendees} from "../../../queries/useGetAttendees.ts"; -import {useGetEvent} from "../../../queries/useGetEvent.ts"; -import {useCheckInAttendee} from "../../../mutations/useCheckInAttendee.ts"; -import {Attendee, QueryFilters} from "../../../types.ts"; -import {showError, showSuccess} from "../../../utilites/notifications.tsx"; -import {AxiosError} from "axios"; -import {IconQrcode} from "@tabler/icons-react"; -import {QRScannerComponent} from "./QrScanner.tsx"; -import {t, Trans} from "@lingui/macro"; -import {useGetEventCheckInStats} from "../../../queries/useGetEventCheckInStats.ts"; - -export const AttendeesCheckInTable = () => { - const {eventId} = useParams(); - const [searchQuery, setSearchQuery] = useState(''); - const [searchQueryDebounced] = useDebouncedValue(searchQuery, 200); - const [qrScannerOpen, setQrScannerOpen] = useState(false); - const {data: {tickets} = {}} = useGetEvent(eventId); - const queryFilters: QueryFilters = { - pageNumber: 1, - query: searchQueryDebounced, - perPage: 100, - filterFields: { - status: ['ACTIVE'], - }, - }; - const attendeesQuery = useGetAttendees(eventId, queryFilters); - const attendees = attendeesQuery?.data?.data; - const mutation = useCheckInAttendee(); - const {data: eventStats} = useGetEventCheckInStats(eventId); - - const handleCheckInToggle = (checked: boolean, attendee: Attendee) => { - mutation.mutate({ - eventId: eventId, - attendeePublicId: attendee.public_id, - action: checked ? 'check_in' : 'check_out', - pagination: queryFilters, - }, { - onSuccess: ({data: attendee}, variables) => { - showSuccess(Successfully - checked {attendee.first_name} {attendee.last_name} {variables.action === 'check_in' ? 'in' : 'out'} - ); - }, - onError: (error, variables) => { - if (error instanceof AxiosError) { - showError(error?.response?.data.message || - Unable to {variables.action ? t`check in` : t`check out`} attendee); - } - } - }) - } - - const handleQrCheckIn = (attendeePublicId: string, onRequestComplete?: () => void) => { - mutation.mutate({ - eventId: eventId, - attendeePublicId: attendeePublicId, - action: 'check_in', - pagination: queryFilters, - }, { - onSuccess: ({data: attendee}, variables) => { - if (onRequestComplete) { - onRequestComplete() - } - showSuccess(Successfully - checked {attendee.first_name} {attendee.last_name} {variables.action === 'check_in' ? 'in' : 'out'} - ); - }, - onError: (error, variables) => { - if (onRequestComplete) { - onRequestComplete() - } - if (error instanceof AxiosError) { - showError(error?.response?.data.message || - Unable to {variables.action ? t`check in` : t`check out`} attendee); - } - } - }) - } - - const Attendees = () => { - const Container = () => { - if (attendeesQuery.isFetching || !attendees || !tickets) { - return ( -
- -
- ) - } - - if (attendees.length === 0) { - return ( -
- No attendees to show. -
- ); - } - - return ( -
- {attendees.map(attendee => { - return ( - -
-
- {attendee.first_name} {attendee.last_name} -
-
- {attendee.public_id} -
-
- {tickets.find(ticket => ticket.id === attendee.ticket_id)?.title} -
-
-
- handleCheckInToggle(event.target.checked, attendee)} - /> -
-
- ) - })} -
- ) - } - - return ( -
- -
- ); - } - - return ( - <> - -
- - {t`Check In`}{' '} - {eventStats && ( - - - {eventStats && `${eventStats.total_checked_in_attendees}/${eventStats.total_attendees}`} checked in - - - )} - - -
- setSearchQuery(event.target.value)} - onClear={() => setSearchQuery('')} - placeholder={t`Seach by name, order #, attendee # or email...`} - /> - - setQrScannerOpen(true)}> - - -
-
-
- - {qrScannerOpen && ( - setQrScannerOpen(false)} - fullScreen - radius={0} - transitionProps={{transition: 'fade', duration: 200}} - padding={'none'} - > - - - setQrScannerOpen(false)} - /> - - - )} - - ); -}; diff --git a/frontend/src/components/layouts/CheckIn/index.tsx b/frontend/src/components/layouts/CheckIn/index.tsx index 9b5e65e9b..c932b8fa4 100644 --- a/frontend/src/components/layouts/CheckIn/index.tsx +++ b/frontend/src/components/layouts/CheckIn/index.tsx @@ -100,14 +100,14 @@ const CheckIn = () => { ) } - const handleQrCheckIn = (attendeePublicId: string, onRequestComplete: () => void, onFailure: () => void) => { + const handleQrCheckIn = (attendeePublicId: string, onRequestComplete: (didSucceed: void) => void, onFailure: () => void) => { checkInMutation.mutate({ checkInListShortId: checkInListShortId, attendeePublicId: attendeePublicId, }, { onSuccess: ({errors}) => { if (onRequestComplete) { - onRequestComplete() + onRequestComplete(!(errors && errors[attendeePublicId])) } // Show error if there is an error for this specific attendee // It's a bulk endpoint, so even if there's an error it returns a 200 From eef5657968363a96b5d18b53e8cb7cc16d2d90c6 Mon Sep 17 00:00:00 2001 From: Graham Blair Date: Sun, 27 Oct 2024 00:18:12 -0700 Subject: [PATCH 2/3] Modify colors, add in-progress --- .../QrScanner.module.scss | 12 +++++++----- .../common/AttendeeCheckInTable/QrScanner.tsx | 19 ++++++++++++------- .../src/components/layouts/CheckIn/index.tsx | 2 +- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/common/AttendeeCheckInTable/QrScanner.module.scss b/frontend/src/components/common/AttendeeCheckInTable/QrScanner.module.scss index 189811d67..3cb3e6faf 100644 --- a/frontend/src/components/common/AttendeeCheckInTable/QrScanner.module.scss +++ b/frontend/src/components/common/AttendeeCheckInTable/QrScanner.module.scss @@ -62,7 +62,7 @@ position: absolute; animation: colorfulBorder 10s infinite; border-radius: 10px; - outline: solid 50vmax #472e7840; + outline: solid 50vmax rgb(71 46 120 / 50%); transition: outline-color .2s ease-out; min-width: 200px; min-height: 200px; @@ -74,13 +74,15 @@ } .scannerOverlay.success { - outline: solid 50vmax rgb(72 193 72 / 50%); - + outline: solid 50vmax rgb(80 148 80 / 75%); } .scannerOverlay.failure { - outline: solid 50vmax rgb(193 72 72 / 50%); - + outline: solid 50vmax rgb(193 72 72 / 75%); + } + + .scannerOverlay.checkingIn { + outline: solid 50vmax rgb(172 158 85 / 60%); } video { diff --git a/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx b/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx index a5251035e..12f5fddbe 100644 --- a/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx +++ b/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx @@ -25,7 +25,7 @@ export const QRScannerComponent = (props: QRScannerComponentProps) => { const latestProcessedAttendeeIdsRef = useRef([]); const [currentAttendeeId, setCurrentAttendeeId] = useState(null); - const [debouncedAttendeeId] = useDebouncedValue(currentAttendeeId, 500); + const [debouncedAttendeeId] = useDebouncedValue(currentAttendeeId, 1000); const [isScanFailed, setIsScanFailed] = useState(false); const [isScanSucceeded, setIsScanSucceeded] = useState(false); @@ -56,12 +56,19 @@ export const QRScannerComponent = (props: QRScannerComponentProps) => { const latestProcessedAttendeeIds = latestProcessedAttendeeIdsRef.current; const alreadyScanned = latestProcessedAttendeeIds.includes(debouncedAttendeeId); + if (isScanSucceeded || isScanFailed) { + // wait for the full succeed/fail outline transition to complete + return; + } + if (alreadyScanned) { showError(t`You already scanned this ticket`); + setIsScanFailed(true); setInterval(function() { setIsScanFailed(false); - }, 1000); + }, 500); + return; } @@ -72,18 +79,16 @@ export const QRScannerComponent = (props: QRScannerComponentProps) => { setProcessedAttendeeIds(prevIds => [...prevIds, debouncedAttendeeId]); setCurrentAttendeeId(null); - console.log(`Did it succeed? ${didSucceed}`) - console.log(this); if (didSucceed) { setIsScanSucceeded(true); setInterval(function() { setIsScanSucceeded(false); - }, 1000); + }, 500); } else { setIsScanFailed(true); setInterval(function() { setIsScanFailed(false); - }, 1000); + }, 500); } }, () => { setIsCheckingIn(false); @@ -198,7 +203,7 @@ export const QRScannerComponent = (props: QRScannerComponentProps) => { -
+
); }; diff --git a/frontend/src/components/layouts/CheckIn/index.tsx b/frontend/src/components/layouts/CheckIn/index.tsx index c932b8fa4..cad154968 100644 --- a/frontend/src/components/layouts/CheckIn/index.tsx +++ b/frontend/src/components/layouts/CheckIn/index.tsx @@ -100,7 +100,7 @@ const CheckIn = () => { ) } - const handleQrCheckIn = (attendeePublicId: string, onRequestComplete: (didSucceed: void) => void, onFailure: () => void) => { + const handleQrCheckIn = (attendeePublicId: string, onRequestComplete: (didSucceed: bool) => void, onFailure: () => void) => { checkInMutation.mutate({ checkInListShortId: checkInListShortId, attendeePublicId: attendeePublicId, From 60eb4dbd09474574f4f262fb6140dc17309249c3 Mon Sep 17 00:00:00 2001 From: Dave Earley Date: Sun, 27 Oct 2024 18:53:11 -0700 Subject: [PATCH 3/3] type fix --- .../src/components/common/AttendeeCheckInTable/QrScanner.tsx | 3 +-- frontend/src/components/layouts/CheckIn/index.tsx | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx b/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx index 12f5fddbe..6e309ed29 100644 --- a/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx +++ b/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx @@ -8,7 +8,7 @@ import {showError} from "../../../utilites/notifications.tsx"; import {t, Trans} from "@lingui/macro"; interface QRScannerComponentProps { - onCheckIn: (attendeePublicId: string, onRequestComplete: (didSucceed: bool) => void, onFailure: () => void) => void; + onCheckIn: (attendeePublicId: string, onRequestComplete: (didSucceed: boolean) => void, onFailure: () => void) => void; onClose: () => void; } @@ -57,7 +57,6 @@ export const QRScannerComponent = (props: QRScannerComponentProps) => { const alreadyScanned = latestProcessedAttendeeIds.includes(debouncedAttendeeId); if (isScanSucceeded || isScanFailed) { - // wait for the full succeed/fail outline transition to complete return; } diff --git a/frontend/src/components/layouts/CheckIn/index.tsx b/frontend/src/components/layouts/CheckIn/index.tsx index cad154968..ed3ce836c 100644 --- a/frontend/src/components/layouts/CheckIn/index.tsx +++ b/frontend/src/components/layouts/CheckIn/index.tsx @@ -100,7 +100,7 @@ const CheckIn = () => { ) } - const handleQrCheckIn = (attendeePublicId: string, onRequestComplete: (didSucceed: bool) => void, onFailure: () => void) => { + const handleQrCheckIn = (attendeePublicId: string, onRequestComplete: (didSucceed: boolean) => void, onFailure: () => void) => { checkInMutation.mutate({ checkInListShortId: checkInListShortId, attendeePublicId: attendeePublicId, @@ -211,7 +211,6 @@ const CheckIn = () => { />) } - if (checkInList?.is_expired) { return (