Skip to content

Commit 5379340

Browse files
committed
Refactor components
1 parent 4bf1a65 commit 5379340

34 files changed

+1494
-1299
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {Anchor, Button} from "@mantine/core";
2+
import {t, Trans} from "@lingui/macro";
3+
import classes from "./QrScanner.module.scss";
4+
5+
interface PermissionDeniedMessageProps {
6+
onRequestPermission: () => void;
7+
onClose: () => void;
8+
}
9+
10+
export const PermissionDeniedMessage = ({
11+
onRequestPermission,
12+
onClose
13+
}: PermissionDeniedMessageProps) => {
14+
return (
15+
<div className={classes.permissionMessage}>
16+
<Trans>
17+
Camera permission was denied. <Anchor onClick={onRequestPermission}>Request
18+
Permission</Anchor> again,
19+
or if this doesn't work,
20+
you will need to <Anchor target={'_blank'}
21+
href={'https://support.onemob.com/hc/en-us/articles/360037342154-How-do-I-grant-permission-for-Camera-and-Microphone-in-my-web-browser-'}>grant
22+
this page</Anchor> access to your camera in your browser settings.
23+
</Trans>
24+
25+
<div>
26+
<Button color={'green'} mt={20} onClick={onClose} variant={'filled'}>
27+
{t`Close`}
28+
</Button>
29+
</div>
30+
</div>
31+
);
32+
};

frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx

Lines changed: 20 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import {useEffect, useRef, useState} from 'react';
22
import QrScanner from 'qr-scanner';
33
import {useDebouncedValue} from '@mantine/hooks';
44
import classes from './QrScanner.module.scss';
5-
import {IconBulb, IconBulbOff, IconCameraRotate, IconVolume, IconVolumeOff, IconX} from "@tabler/icons-react";
6-
import {Anchor, Button, Menu} from "@mantine/core";
75
import {showError} from "../../../utilites/notifications.tsx";
8-
import {t, Trans} from "@lingui/macro";
6+
import {t} from "@lingui/macro";
7+
import {QrScannerControls} from './QrScannerControls';
8+
import {PermissionDeniedMessage} from './PermissionDeniedMessage';
99

1010
interface QRScannerComponentProps {
1111
onAttendeeScanned: (attendeePublicId: string) => void;
@@ -178,63 +178,37 @@ export const QRScannerComponent = (props: QRScannerComponentProps) => {
178178
};
179179
}, []);
180180

181-
const handleCameraSelection = (camera: QrScanner.Camera) => () => {
181+
const handleCameraSelection = (camera: QrScanner.Camera) => {
182182
return qrScannerRef.current?.setCamera(camera.id)
183183
.then(() => updateFlashAvailability().catch(console.error));
184184
};
185185

186186
return (
187187
<div className={classes.videoContainer}>
188188
{permissionDenied && (
189-
<div className={classes.permissionMessage}>
190-
<Trans>
191-
Camera permission was denied. <Anchor onClick={requestPermission}>Request
192-
Permission</Anchor> again,
193-
or if this doesn't work,
194-
you will need to <Anchor target={'_blank'}
195-
href={'https://support.onemob.com/hc/en-us/articles/360037342154-How-do-I-grant-permission-for-Camera-and-Microphone-in-my-web-browser-'}>grant
196-
this page</Anchor> access to your camera in your browser settings.
197-
</Trans>
198-
199-
<div>
200-
<Button color={'green'} mt={20} onClick={handleClose} variant={'filled'}>
201-
{t`Close`}
202-
</Button>
203-
</div>
204-
</div>
189+
<PermissionDeniedMessage
190+
onRequestPermission={requestPermission}
191+
onClose={handleClose}
192+
/>
205193
)}
206194

207195
<video className={classes.video} ref={videoRef}></video>
208196

209-
<Button onClick={handleFlashToggle} variant={'transparent'} className={classes.flashToggle}>
210-
{!isFlashAvailable && <IconBulbOff color={'#ffffff95'} size={30}/>}
211-
{isFlashAvailable && <IconBulb color={isFlashOn ? 'yellow' : '#ffffff95'} size={30}/>}
212-
</Button>
213-
<Button onClick={handleSoundToggle} variant={'transparent'} className={classes.soundToggle}>
214-
{isSoundOn && <IconVolume color={'#ffffff95'} size={30}/>}
215-
{!isSoundOn && <IconVolumeOff color={'#ffffff95'} size={30}/>}
216-
</Button>
197+
<QrScannerControls
198+
isFlashAvailable={isFlashAvailable}
199+
isFlashOn={isFlashOn}
200+
isSoundOn={isSoundOn}
201+
cameraList={cameraList}
202+
onFlashToggle={handleFlashToggle}
203+
onSoundToggle={handleSoundToggle}
204+
onCameraSelect={handleCameraSelection}
205+
onClose={handleClose}
206+
/>
207+
217208
<audio ref={scanSuccessAudioRef} src="/sounds/scan-success.wav"/>
218209
<audio ref={scanErrorAudioRef} src="/sounds/scan-error.wav"/>
219210
<audio ref={scanInProgressAudioRef} src="/sounds/scan-in-progress.wav"/>
220-
<Button onClick={handleClose} variant={'transparent'} className={classes.closeButton}>
221-
<IconX color={'#ffffff95'} size={30}/>
222-
</Button>
223-
<Button variant={'transparent'} className={classes.switchCameraButton}>
224-
<Menu shadow="md" width={200}>
225-
<Menu.Target>
226-
<IconCameraRotate color={'#ffffff95'} size={30}/>
227-
</Menu.Target>
228-
<Menu.Dropdown>
229-
<Menu.Label>{t`Select Camera`}</Menu.Label>
230-
{cameraList?.map((camera, index) => (
231-
<Menu.Item key={index} onClick={handleCameraSelection(camera)}>
232-
{camera.label}
233-
</Menu.Item>
234-
))}
235-
</Menu.Dropdown>
236-
</Menu>
237-
</Button>
211+
238212
<div className={`${classes.scannerOverlay} ${isScanSucceeded ? classes.success : ""} ${isScanFailed ? classes.failure : ""} ${isCheckingIn ? classes.checkingIn : ""}`}/>
239213
</div>
240214
);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {Button, Menu} from "@mantine/core";
2+
import {IconBulb, IconBulbOff, IconCameraRotate, IconVolume, IconVolumeOff, IconX} from "@tabler/icons-react";
3+
import {t} from "@lingui/macro";
4+
import QrScanner from "qr-scanner";
5+
import classes from "./QrScanner.module.scss";
6+
7+
interface QrScannerControlsProps {
8+
isFlashAvailable: boolean;
9+
isFlashOn: boolean;
10+
isSoundOn: boolean;
11+
cameraList: QrScanner.Camera[] | undefined;
12+
onFlashToggle: () => void;
13+
onSoundToggle: () => void;
14+
onCameraSelect: (camera: QrScanner.Camera) => void;
15+
onClose: () => void;
16+
}
17+
18+
export const QrScannerControls = ({
19+
isFlashAvailable,
20+
isFlashOn,
21+
isSoundOn,
22+
cameraList,
23+
onFlashToggle,
24+
onSoundToggle,
25+
onCameraSelect,
26+
onClose
27+
}: QrScannerControlsProps) => {
28+
return (
29+
<>
30+
<Button onClick={onFlashToggle} variant={'transparent'} className={classes.flashToggle}>
31+
{!isFlashAvailable && <IconBulbOff color={'#ffffff95'} size={30}/>}
32+
{isFlashAvailable && <IconBulb color={isFlashOn ? 'yellow' : '#ffffff95'} size={30}/>}
33+
</Button>
34+
<Button onClick={onSoundToggle} variant={'transparent'} className={classes.soundToggle}>
35+
{isSoundOn && <IconVolume color={'#ffffff95'} size={30}/>}
36+
{!isSoundOn && <IconVolumeOff color={'#ffffff95'} size={30}/>}
37+
</Button>
38+
<Button onClick={onClose} variant={'transparent'} className={classes.closeButton}>
39+
<IconX color={'#ffffff95'} size={30}/>
40+
</Button>
41+
<Button variant={'transparent'} className={classes.switchCameraButton}>
42+
<Menu shadow="md" width={200}>
43+
<Menu.Target>
44+
<IconCameraRotate color={'#ffffff95'} size={30}/>
45+
</Menu.Target>
46+
<Menu.Dropdown>
47+
<Menu.Label>{t`Select Camera`}</Menu.Label>
48+
{cameraList?.map((camera, index) => (
49+
<Menu.Item key={index} onClick={() => onCameraSelect(camera)}>
50+
{camera.label}
51+
</Menu.Item>
52+
))}
53+
</Menu.Dropdown>
54+
</Menu>
55+
</Button>
56+
</>
57+
);
58+
};
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import {Button, Loader} from "@mantine/core";
2+
import {IconTicket} from "@tabler/icons-react";
3+
import {t} from "@lingui/macro";
4+
import {Attendee} from "../../../types.ts";
5+
import classes from "../../layouts/CheckIn/CheckIn.module.scss";
6+
7+
interface AttendeeListProps {
8+
attendees: Attendee[] | undefined;
9+
products: { id: number; title: string; }[] | undefined;
10+
isLoading: boolean;
11+
isCheckInPending: boolean;
12+
isDeletePending: boolean;
13+
allowOrdersAwaitingOfflinePaymentToCheckIn: boolean;
14+
onCheckInToggle: (attendee: Attendee) => void;
15+
onClickSound?: () => void;
16+
}
17+
18+
export const AttendeeList = ({
19+
attendees,
20+
products,
21+
isLoading,
22+
isCheckInPending,
23+
isDeletePending,
24+
allowOrdersAwaitingOfflinePaymentToCheckIn,
25+
onCheckInToggle,
26+
onClickSound
27+
}: AttendeeListProps) => {
28+
const checkInButtonText = (attendee: Attendee) => {
29+
if (!allowOrdersAwaitingOfflinePaymentToCheckIn && attendee.status === 'AWAITING_PAYMENT') {
30+
return t`Cannot Check In`;
31+
}
32+
33+
if (attendee.check_in) {
34+
return t`Check Out`;
35+
}
36+
37+
return t`Check In`;
38+
};
39+
40+
const getButtonColor = (attendee: Attendee) => {
41+
if (attendee.check_in) {
42+
return 'red';
43+
}
44+
if (attendee.status === 'AWAITING_PAYMENT' && !allowOrdersAwaitingOfflinePaymentToCheckIn) {
45+
return 'gray';
46+
}
47+
return 'teal';
48+
};
49+
50+
if (isLoading || !attendees || !products) {
51+
return (
52+
<div className={classes.loading}>
53+
<Loader size={40}/>
54+
</div>
55+
);
56+
}
57+
58+
if (attendees.length === 0) {
59+
return (
60+
<div className={classes.noResults}>
61+
No attendees to show.
62+
</div>
63+
);
64+
}
65+
66+
return (
67+
<div className={classes.attendees}>
68+
{attendees.map(attendee => {
69+
const isAttendeeAwaitingPayment = attendee.status === 'AWAITING_PAYMENT';
70+
71+
return (
72+
<div className={classes.attendee} key={attendee.public_id}>
73+
<div className={classes.details}>
74+
<div>
75+
<b>{attendee.first_name} {attendee.last_name}</b>
76+
</div>
77+
<div style={{fontSize: '0.8em', color: '#555'}}>
78+
{attendee.email}
79+
</div>
80+
{isAttendeeAwaitingPayment && (
81+
<div className={classes.awaitingPayment}>
82+
{t`Awaiting payment`}
83+
</div>
84+
)}
85+
<div>
86+
<span>{attendee.public_id}</span>
87+
</div>
88+
<div className={classes.product}>
89+
<IconTicket
90+
size={15}/> {products.find(product => product.id === attendee.product_id)?.title}
91+
</div>
92+
</div>
93+
<div className={classes.actions}>
94+
<Button
95+
onClick={() => {
96+
onClickSound?.();
97+
onCheckInToggle(attendee);
98+
}}
99+
disabled={isCheckInPending || isDeletePending}
100+
loading={isCheckInPending || isDeletePending}
101+
color={getButtonColor(attendee)}
102+
>
103+
{checkInButtonText(attendee)}
104+
</Button>
105+
</div>
106+
</div>
107+
);
108+
})}
109+
</div>
110+
);
111+
};
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {Modal, Progress} from "@mantine/core";
2+
import {Trans} from "@lingui/macro";
3+
import Truncate from "../Truncate";
4+
import {CheckInList} from "../../../types.ts";
5+
import classes from "../../layouts/CheckIn/CheckIn.module.scss";
6+
7+
interface CheckInInfoModalProps {
8+
isOpen: boolean;
9+
checkInList: CheckInList | undefined;
10+
onClose: () => void;
11+
}
12+
13+
export const CheckInInfoModal = ({
14+
isOpen,
15+
checkInList,
16+
onClose
17+
}: CheckInInfoModalProps) => {
18+
if (!checkInList) return null;
19+
20+
return (
21+
<Modal.Root
22+
opened={isOpen}
23+
radius={0}
24+
onClose={onClose}
25+
transitionProps={{transition: 'fade', duration: 200}}
26+
padding={'none'}
27+
>
28+
<Modal.Overlay/>
29+
<Modal.Content>
30+
<Modal.Header>
31+
<Modal.Title>
32+
<Truncate text={checkInList.name} length={30}/>
33+
</Modal.Title>
34+
<Modal.CloseButton/>
35+
</Modal.Header>
36+
<div className={classes.infoModal}>
37+
<div className={classes.checkInCount}>
38+
<>
39+
<h4>
40+
<Trans>
41+
{`${checkInList.checked_in_attendees}/${checkInList.total_attendees}`} checked in
42+
</Trans>
43+
</h4>
44+
45+
<Progress
46+
value={checkInList.checked_in_attendees / checkInList.total_attendees * 100}
47+
color={'teal'}
48+
size={'xl'}
49+
className={classes.progressBar}
50+
/>
51+
</>
52+
</div>
53+
54+
{checkInList.description && (
55+
<div className={classes.description}>
56+
{checkInList.description}
57+
</div>
58+
)}
59+
</div>
60+
</Modal.Content>
61+
</Modal.Root>
62+
);
63+
};

0 commit comments

Comments
 (0)