Skip to content

Commit

Permalink
feat(tupaiaWeb): RN-1371: QR code scanner (#5892)
Browse files Browse the repository at this point in the history
* Button

* Reorganise files

* Working scanner

* Working scanner and types

* Types

* fixes

* update error handling

* Update EntitySearch.tsx

---------

Co-authored-by: Andrew <vanbeekandrew@gmail.com>
Co-authored-by: Tom Caiger <caigertom@gmail.com>
  • Loading branch information
3 people authored Oct 7, 2024
1 parent 84846fa commit 2fa1912
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 18 deletions.
1 change: 1 addition & 0 deletions packages/tupaia-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"react-dom": "^16.13.1",
"react-hook-form": "^6.15.1",
"react-leaflet": "^3.2.1",
"react-qr-reader": "^3.0.0-beta-1",
"react-router": "6.3.0",
"react-router-dom": "6.3.0",
"react-slick": "^0.30.2",
Expand Down
10 changes: 6 additions & 4 deletions packages/tupaia-web/src/features/EntitySearch/EntitySearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,25 @@ import { SearchResults } from './SearchResults';
import { gaEvent } from '../../utils';

const Container = styled.div`
position: relative;
display: flex;
z-index: 1;
flex-direction: column;
align-items: center;
margin-right: 1rem;
margin-top: 0.6rem;
width: 19rem;
position: relative;
@media screen and (max-width: ${MOBILE_BREAKPOINT}) {
width: auto;
margin: 0;
position: initial;
}
`;

const ResultsWrapper = styled.div`
position: absolute;
top: 100%;
left: 0;
right: 0;
background: ${({ theme }) => theme.palette.background.paper};
padding: 0 0.3rem 0.625rem;
width: calc(100% + 5px);
Expand All @@ -40,14 +42,14 @@ const ResultsWrapper = styled.div`
overflow-y: auto;
@media screen and (max-width: ${MOBILE_BREAKPOINT}) {
position: fixed;
width: 100%;
top: ${TOP_BAR_HEIGHT_MOBILE};
left: 0;
right: 0;
z-index: 1;
min-height: calc(100vh - ${TOP_BAR_HEIGHT_MOBILE});
max-height: calc(100vh - ${TOP_BAR_HEIGHT_MOBILE});
border-radius: 0;
position: fixed;
}
`;

Expand Down
33 changes: 20 additions & 13 deletions packages/tupaia-web/src/features/EntitySearch/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import styled from 'styled-components';
import { TextField, TextFieldProps } from '@material-ui/core';
import SearchIcon from '@material-ui/icons/Search';
import { Close, Search } from '@material-ui/icons';
import { MOBILE_BREAKPOINT, TOP_BAR_HEIGHT_MOBILE } from '../../constants';
import { IconButton } from '@tupaia/ui-components';
import { MOBILE_BREAKPOINT, TOP_BAR_HEIGHT_MOBILE } from '../../constants';
import { QRCodeScanner } from '../QRCodeScanner';

const SearchInput = styled(TextField).attrs({
variant: 'outlined',
placeholder: 'Search location',
placeholder: 'Search location...',
fullWidth: true,
InputProps: {
startAdornment: <SearchIcon />,
Expand Down Expand Up @@ -61,14 +62,9 @@ const SearchInput = styled(TextField).attrs({
`;

const MobileCloseButton = styled(IconButton)`
display: none;
@media screen and (max-width: ${MOBILE_BREAKPOINT}) {
display: block;
position: absolute;
top: 0.1rem;
right: 0.1rem;
z-index: 1;
}
top: 0.1rem;
right: 0.1rem;
z-index: 1;
`;

const Container = styled.div<{
Expand All @@ -86,6 +82,8 @@ const Container = styled.div<{
height: ${TOP_BAR_HEIGHT_MOBILE};
// Place on top of the hamburger menu on mobile
z-index: 1;
display: flex;
background: ${({ theme }) => theme.palette.background.paper};
}
`;

Expand All @@ -95,6 +93,12 @@ const MobileOpenButton = styled(IconButton)`
display: block;
}
`;
const MobileWrapper = styled.div`
display: none;
@media screen and (max-width: ${MOBILE_BREAKPOINT}) {
display: flex;
}
`;

interface SearchBarProps {
value?: string;
Expand Down Expand Up @@ -140,9 +144,12 @@ export const SearchBar = ({ value = '', onChange, onFocusChange, onClose }: Sear
onFocus={() => onFocusChange(true)}
inputRef={inputRef}
/>
<MobileCloseButton onClick={handleClickClose} color="default">
<Close />
</MobileCloseButton>
<MobileWrapper>
<QRCodeScanner onCloseEntitySearch={handleClickClose} />
<MobileCloseButton onClick={handleClickClose} color="default">
<Close />
</MobileCloseButton>
</MobileWrapper>
</Container>
</>
);
Expand Down
150 changes: 150 additions & 0 deletions packages/tupaia-web/src/features/QRCodeScanner/QRCodeScanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import React, { useState } from 'react';
import styled from 'styled-components';
import { Button, IconButton, SmallAlert } from '@tupaia/ui-components';
import { QrReader } from 'react-qr-reader';
import { get } from '../../api';
// This import is the actual type that QrReader uses
import { Result } from '@zxing/library';
import { QRScanIcon } from './QRScanIcon';
import { ClickAwayListener, Typography } from '@material-ui/core';
import { Close } from '@material-ui/icons';
import { generatePath, useLocation, useNavigate, useParams } from 'react-router';
import { ROUTE_STRUCTURE } from '../../constants';

const QRScanButton = styled(Button).attrs({
startIcon: <QRScanIcon />,
variant: 'text',
})`
background: ${({ theme }) => theme.palette.background.paper};
text-transform: none;
font-size: 0.875rem;
font-weight: 400;
padding-inline: 0.5rem;
white-space: nowrap;
height: 100%;
`;

const ScannerWrapper = styled.div`
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
z-index: 10;
background: ${({ theme }) => theme.palette.background.paper};
padding-inline: 1.4rem;
padding-block: 1.2rem;
`;

const CloseButton = styled(IconButton)`
.MuiSvgIcon-root {
font-size: 1rem;
}
padding: 0.5rem;
`;

const Header = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;

const Title = styled(Typography).attrs({
variant: 'h1',
})`
font-size: 0.75rem;
font-weight: 500;
`;

export const QRCodeScanner = ({ onCloseEntitySearch }: { onCloseEntitySearch: () => void }) => {
const { projectCode, dashboardName } = useParams();
const navigate = useNavigate();
const location = useLocation();
const [isQRScannerOpen, setIsQRScannerOpen] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);

const toggleQRScanner = () => {
setIsQRScannerOpen(!isQRScannerOpen);
setErrorMessage(null);
};

const handleScan = async (data?: Result | null, error?: Error | null) => {
if (error?.message) {
setErrorMessage(error.message);
}

if (!data) {
return;
}

const text = data.getText();
const entityId = text.replace('entity-', '');
let entityCode: string;

try {
const results = await get(`entities/${projectCode}/${projectCode}`, {
params: {
filter: { id: entityId },
fields: ['code'],
},
});
const entity = results[0] ?? null;

if (!entity) {
setErrorMessage(
'No matching entity found in selected project. Please try another QR code, or check your project selection.',
);
return;
}
entityCode = entity.code;
// reset error message
setErrorMessage(null);
} catch (e) {
setErrorMessage('Error fetching entity details. Please refresh the page and try again.');
return;
}

const path = generatePath(ROUTE_STRUCTURE, {
projectCode,
dashboardName,
entityCode,
});

// navigate to the entity page and close the scanner and entity search
navigate({
...location,
pathname: path,
});

setIsQRScannerOpen(false);
onCloseEntitySearch();
};

return (
<>
<QRScanButton onClick={toggleQRScanner}>Scan ID</QRScanButton>
{isQRScannerOpen && (
<ClickAwayListener onClickAway={toggleQRScanner}>
<ScannerWrapper>
<Header>
<Title>Scan the location ID QR code using your camera</Title>
<CloseButton onClick={toggleQRScanner}>
<Close />
</CloseButton>
</Header>
{errorMessage && <SmallAlert severity="error">{errorMessage}</SmallAlert>}
<QrReader
// use the camera facing the environment (back camera)
constraints={{ facingMode: 'environment' }}
onResult={handleScan}
/>
</ScannerWrapper>
</ClickAwayListener>
)}
</>
);
};
24 changes: 24 additions & 0 deletions packages/tupaia-web/src/features/QRCodeScanner/QRScanIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import React from 'react';
import { SvgIcon, SvgIconProps } from '@material-ui/core';

export const QRScanIcon = (props: SvgIconProps) => {
return (
<SvgIcon
{...props}
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.92461 9.4373H9.36824V4.99367H4.92461V9.4373ZM5.82461 5.89355H8.46824V8.53717H5.82461V5.89355ZM14.9376 15.0065H10.1447V13.5928H11.0447V14.1067H12.0916V13.5928H12.9916V14.1067H14.0377V13.5928H14.9377L14.9377 15.0065H14.9376ZM11.0447 12.5213H10.1447V10.2139H13.3427V11.1139H11.0447V12.5213ZM17.2 4.65605V8.1438H16.3V4.65605C16.3 4.12924 15.871 3.70054 15.3437 3.70054H11.856V2.80054H15.3437C16.3672 2.80054 17.2 3.63292 17.2 4.65605ZM3.69999 8.1438H2.79999V4.65605C2.79999 3.6329 3.6328 2.80055 4.65624 2.80055H8.14399V3.70055H4.65624C4.12899 3.70055 3.69999 4.12911 3.69999 4.65606V8.1438ZM4.65624 16.2996H8.14399V17.1995H4.65624C3.6328 17.1995 2.79999 16.3669 2.79999 15.3433V11.8563H3.69999V15.3433C3.69999 15.8707 4.12899 16.2996 4.65624 16.2996ZM16.3 11.8563H17.2V15.3433C17.2 16.3669 16.3672 17.1995 15.3437 17.1995H11.856V16.2996H15.3437C15.871 16.2996 16.3 15.8705 16.3 15.3433V11.8563ZM4.92461 15.0064H9.36824V10.5629H4.92461V15.0064ZM5.82461 11.4628H8.46824V14.1064H5.82461V11.4628ZM14.9376 4.99355H10.494V9.43717H14.9376V4.99355ZM14.0376 8.53717H11.394V5.89342H14.0376V8.53717ZM11.7396 12.0712H14.9376V12.9712H11.7396V12.0712ZM14.9376 11.1775H14.0376V10.2136H14.9376V11.1775ZM6.71749 12.3558H7.57535V13.2136H6.71749V12.3558ZM6.71749 6.78637H7.57535V7.64424H6.71749V6.78637ZM13.1447 7.64424H12.2869V6.78637H13.1447V7.64424Z"
fill="white"
/>
</SvgIcon>
);
};
21 changes: 21 additions & 0 deletions packages/tupaia-web/src/features/QRCodeScanner/QrCodeButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import React from 'react';
import styled from 'styled-components';
import { Button } from '@tupaia/ui-components';
import { QRScanIcon } from './QRScanIcon';

export const QrScanButton = styled(Button).attrs({
startIcon: <QRScanIcon />,
variant: 'text',
})`
background: ${({ theme }) => theme.palette.background.paper};
text-transform: none;
font-size: 0.875rem;
font-weight: 400;
padding-inline: 0.5rem;
white-space: nowrap;
height: 100%;
`;
5 changes: 5 additions & 0 deletions packages/tupaia-web/src/features/QRCodeScanner/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
export { QRCodeScanner } from './QRCodeScanner';
Loading

0 comments on commit 2fa1912

Please sign in to comment.