Skip to content

Commit bb3afb4

Browse files
helenailicashleyylicamryynfirmianaw
authored
Hi/show qr code (#53)
* displaying qr code * loading screen route and automatic redirect * adding points logging * Al/qr code modal (#43) * Add Show QR button and draft QR modal * Add empty onClick handlers * Left align QR code modal content (#37) --------- Co-authored-by: Ashley Li <[email protected]> * Add draft loading screen * Finalize error page (#41) * Hi/downloadable qr code (#42) * Add edit event modal * Add edit event modal * Fix create event modal and successful edit message * Cl/edit icon (#34) * adding explicit logout login button * removing automatic redirect * Add edit icon to events only visible to officers * Remove extraneous commits * Make it conditional to officers * Update api.ts * fix times * fix login redirect * merge edit icon and button * remove excess code * fix event modal close button * fix logo width mobile * fix default points value * remve value * adding download png and download svg feature to url * Update api.ts * Update api.ts * Hi/login logout button (#33) * adding explicit logout login button * removing automatic redirect * fixing temporary error issue when logged out * Update api.ts * fix err unknown * fixing qr code quality --------- Co-authored-by: Ashley Li <[email protected]> Co-authored-by: Camryn <[email protected]> Co-authored-by: Firmiana <[email protected]> Co-authored-by: Firmiana Wang <[email protected]> * Remove success console logs (#41) * Modify loading screen timing * Al/change qr color (#48) * Add draft QR code color switcher * Clean up code * Modify toggle color * Add success page * Style adds success * Fix artificial loading screen delay --------- Co-authored-by: ashleyyli <[email protected]> Co-authored-by: Ashley Li <[email protected]> Co-authored-by: Camryn <[email protected]> Co-authored-by: Firmiana <[email protected]> Co-authored-by: Firmiana Wang <[email protected]> Co-authored-by: camryyn <[email protected]>
1 parent eab0320 commit bb3afb4

File tree

14 files changed

+6210
-2905
lines changed

14 files changed

+6210
-2905
lines changed

package-lock.json

Lines changed: 5628 additions & 2876 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,24 @@
2626
},
2727
"dependencies": {
2828
"@chakra-ui/react": "^2.4.9",
29+
"@chakra-ui/theme-utils": "^2.0.21",
2930
"@emotion/react": "^11.10.5",
3031
"@emotion/styled": "^11.10.5",
3132
"axios": "^1.2.3",
3233
"cors": "^2.8.5",
3334
"framer-motion": "^8.5.0",
3435
"gh-pages": "^6.1.1",
3536
"moment": "^2.29.4",
37+
"qrcode.react": "^4.1.0",
3638
"react": "^18.2.0",
39+
"react-confetti": "^6.2.2",
3740
"react-dark-mode-toggle-2": "^2.0.8",
3841
"react-dom": "^18.2.0",
3942
"react-icons": "^4.7.1",
4043
"react-query": "^3.39.2",
4144
"react-router-dom": "^6.7.0",
42-
"react-toastify": "^9.1.1"
45+
"react-toastify": "^9.1.1",
46+
"react-use": "^17.6.0"
4347
},
4448
"devDependencies": {
4549
"@types/react": "^18.0.26",

src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import 'react-toastify/dist/ReactToastify.css';
66
import CheckIn from './pages/CheckIn';
77
import Points from './pages/Points';
88
import Events from './pages/Events';
9+
import LoadingScreen from './pages/LoadingScreen';
910
import NavbarLayout from './layouts/NavbarLayout';
11+
import SuccessPage from './pages/LoadingScreen/success';
1012

1113
const App = (): React.ReactElement => {
1214
return (
@@ -17,6 +19,8 @@ const App = (): React.ReactElement => {
1719
<Route path="/" element={<CheckIn />} />
1820
<Route path="/points" element={<Points />} />
1921
<Route path="/events" element={<Events />} />
22+
<Route path="/loading/:eventKey" element={<LoadingScreen />} />
23+
<Route path="/success" element={<SuccessPage />} />
2024
</Routes>
2125
</NavbarLayout>
2226
</BrowserRouter>

src/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import axios from 'axios';
33
const instance = axios.create({
44
baseURL: import.meta.env.VITE_BASE_URL
55
? import.meta.env.VITE_BASE_URL
6-
// : 'http://127.0.0.1:3000',
7-
: 'https://points-api.illinoiswcs.org',
6+
: 'http://127.0.0.1:3000',
7+
// 'https://points-api.illinoiswcs.org',
88
withCredentials: true
99
});
1010

src/components/Navbar/index.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ const Navbar = ({ onClose, ...rest }: NavbarProps): React.ReactElement => {
9393
}
9494
};
9595

96+
if (isError) {
97+
console.log(error);
98+
return (
99+
<Box>
100+
<Heading size="lg">Temporary Error</Heading>
101+
</Box>
102+
);
103+
}
104+
96105
return (
97106
<Box
98107
bg={useColorModeValue('white', 'gray.800')}

src/pages/CheckIn/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useState, BaseSyntheticEvent } from 'react';
22
import { VStack, Heading, Text, Button, Input, Box } from '@chakra-ui/react';
3+
import { useNavigate } from 'react-router-dom';
34

45
import axiosInstance from '../../api';
56
import { toastError, toastSuccess } from '../../utils/toast';
@@ -9,6 +10,7 @@ import { Profile } from '../../types/profile';
910
const CheckIn = (): React.ReactElement => {
1011
const [eventKey, setEventKey] = useState('');
1112
const [eventKeyError, setEventKeyError] = useState(false);
13+
const navigate = useNavigate();
1214

1315
const handleChangeKey = (event: BaseSyntheticEvent): void => {
1416
setEventKey(event.target.value);
@@ -42,6 +44,7 @@ const CheckIn = (): React.ReactElement => {
4244
.patch('/profile', { eventKey })
4345
.then((res) => {
4446
toastSuccess(res.data.message);
47+
navigate('/success');
4548
})
4649
.catch((err) => {
4750
toastError(err.response?.data?.message || 'An error occurred');

src/pages/Events/EventModal/index.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
} from '@chakra-ui/react';
2323
import { useMutation } from 'react-query';
2424

25+
import EventQRCode from '../EventQRCode';
26+
2527
import axiosInstance from '../../../api';
2628
import { EventCategoryType, NewEvent, Event } from '../../../types/event';
2729
import { EventModalProps, StringFieldProps, SameDayFieldProps } from './types';
@@ -49,6 +51,7 @@ const EventModal = (props: EventModalProps): React.ReactElement => {
4951
const [success, setSuccess] = useState(false);
5052
const [error, setError] = useState(false);
5153
const [msg, setMsg] = useState('');
54+
const [eventKey, setEventKey] = useState<string | null>(null);
5255

5356
const handleNameChange = (props: StringFieldProps): void => {
5457
setName(props.target.value);
@@ -225,6 +228,7 @@ const EventModal = (props: EventModalProps): React.ReactElement => {
225228
setSuccess(true);
226229
setError(false);
227230
setMsg(`Success! Event key is ${String(res.data.key)}.`);
231+
setEventKey(String(res.data.key));
228232
reloadOnClose();
229233
})
230234
.catch(() => {
@@ -255,6 +259,7 @@ const EventModal = (props: EventModalProps): React.ReactElement => {
255259
'Internal Error: event edit was unsuccessful. ' +
256260
'Please contact the current WCS infra chair for help.'
257261
);
262+
setEventKey(null);
258263
});
259264
}
260265
});
@@ -389,6 +394,14 @@ const EventModal = (props: EventModalProps): React.ReactElement => {
389394
<Alert status="success" variant="left-accent">
390395
<AlertIcon />
391396
{msg}
397+
{eventKey && (
398+
<EventQRCode
399+
eventKey={eventKey}
400+
size={64}
401+
color={'#d4696a'}
402+
inNotification={true}
403+
/>
404+
)}
392405
</Alert>
393406
)}
394407
{error && (

src/pages/Events/EventQRCode.tsx

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import React, { useRef } from 'react';
2+
import { QRCodeSVG } from 'qrcode.react';
3+
import { Button } from '@chakra-ui/react';
4+
5+
interface EventQRCodeProps {
6+
eventKey: string;
7+
size?: number;
8+
color?: string;
9+
inNotification?: boolean;
10+
}
11+
12+
const EventQRCode: React.FC<EventQRCodeProps> = ({
13+
eventKey,
14+
size = 128,
15+
color = '#d4696a', // does this set a default color
16+
inNotification = false
17+
}) => {
18+
// creates react reference that can point to an svg element
19+
// useRef is react hook that creates mutable reference to object
20+
const qrRef = useRef<SVGSVGElement>(null);
21+
22+
const baseUrl = import.meta.env.DEV
23+
? 'http://127.0.0.1:8080' // development frontend URL
24+
: 'https://points.illinoiswcs.org'; // production frontend URL
25+
26+
const loadingUrl = `${baseUrl}/success`; // redirect
27+
28+
// download as svg
29+
const downloadSVG = (): void => {
30+
if (!qrRef.current) {
31+
return;
32+
}
33+
34+
const clonedSvg = qrRef.current.cloneNode(true) as SVGSVGElement;
35+
36+
// set attributes on the cloned SVG
37+
clonedSvg.setAttribute('viewBox', `0 0 ${size} ${size}`);
38+
clonedSvg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
39+
clonedSvg.setAttribute('width', `${size}px`); // Add px unit
40+
clonedSvg.setAttribute('height', `${size}px`); // Add px unit
41+
42+
// add style to force size
43+
clonedSvg.style.width = `${size}px`;
44+
clonedSvg.style.height = `${size}px`;
45+
46+
// turns qr code visual (svg) into xml code
47+
const svgData = new XMLSerializer().serializeToString(qrRef.current);
48+
49+
// creates binary large object (blob) containing svg image data as xml
50+
const svgBlob = new Blob([svgData], {
51+
type: 'image/svg_xml;charset=utf-8'
52+
});
53+
// creates temp url pointing to blob
54+
const svgUrl = URL.createObjectURL(svgBlob);
55+
56+
// link element
57+
const downloadLink = document.createElement('a');
58+
// points link to temp url containing blob info
59+
downloadLink.href = svgUrl;
60+
// names the link
61+
downloadLink.download = `qr-code-${eventKey}.svg`;
62+
// adds link to page
63+
document.body.appendChild(downloadLink);
64+
// clicks link to start download
65+
downloadLink.click();
66+
// removes link from page
67+
document.body.removeChild(downloadLink);
68+
// removes temp url created
69+
URL.revokeObjectURL(svgUrl);
70+
};
71+
72+
// download as png
73+
const downloadPNG = (): void => {
74+
if (!qrRef.current) {
75+
return;
76+
}
77+
78+
// creates drawing canvas in memory
79+
const scale = 4;
80+
const canvas = document.createElement('canvas');
81+
canvas.width = size * scale;
82+
canvas.height = size * scale;
83+
84+
// tools for drawing on canvas
85+
const ctx = canvas.getContext('2d');
86+
if (!ctx) {
87+
return;
88+
}
89+
90+
// smoothing
91+
ctx.imageSmoothingEnabled = true;
92+
ctx.imageSmoothingQuality = 'high';
93+
ctx.scale(scale, scale);
94+
95+
// creates new image object
96+
const svgData = new XMLSerializer().serializeToString(qrRef.current);
97+
const img = new Image();
98+
99+
// after image loads, draws it onto the canvas
100+
img.onload = () => {
101+
ctx.drawImage(img, 0, 0);
102+
103+
// converts into png format
104+
const pngUrl = canvas.toDataURL('image/png');
105+
106+
const downloadLink = document.createElement('a');
107+
downloadLink.href = pngUrl;
108+
downloadLink.download = `qr-code-${eventKey}.png`;
109+
document.body.appendChild(downloadLink);
110+
downloadLink.click();
111+
document.body.removeChild(downloadLink);
112+
};
113+
114+
img.src = 'data:image/svg+xml;base64,' + btoa(svgData);
115+
};
116+
117+
return (
118+
<div>
119+
<div
120+
style={
121+
inNotification
122+
? {
123+
marginLeft: 'auto',
124+
backgroundColor: '#c6f6d5',
125+
padding: '4px',
126+
borderRadius: '4px'
127+
}
128+
: {}
129+
}
130+
>
131+
<QRCodeSVG
132+
ref={qrRef}
133+
value={loadingUrl}
134+
size={size}
135+
fgColor={color}
136+
bgColor={inNotification ? '#d1fae5' : '#ffffff'}
137+
level="H"
138+
title={`QR Code for event ${eventKey}`}
139+
/>
140+
</div>
141+
<div style={{ marginTop: '10px' }}>
142+
<Button onClick={downloadPNG} mr={3} mt={5}>
143+
Download as PNG
144+
</Button>
145+
<Button onClick={downloadSVG} mt={5}>
146+
Download as SVG
147+
</Button>
148+
</div>
149+
</div>
150+
);
151+
};
152+
153+
export default EventQRCode;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React, { useState } from 'react';
2+
import {
3+
Modal,
4+
ModalCloseButton,
5+
ModalHeader,
6+
HStack,
7+
ModalOverlay,
8+
ModalBody,
9+
ModalContent,
10+
Box,
11+
Switch
12+
} from '@chakra-ui/react';
13+
import { QRCodeModalProps } from './types';
14+
import EventQRCode from '../EventQRCode';
15+
16+
const QRCodeModal = (props: QRCodeModalProps): React.ReactElement => {
17+
const [isToggled, toggleIsToggled] = useState(true);
18+
const { open, event, toggleModal } = props;
19+
20+
const handleToggleColor = (): void => {
21+
toggleIsToggled(!isToggled);
22+
};
23+
24+
const clearAndToggle = (): void => {
25+
toggleModal();
26+
};
27+
28+
return (
29+
<Modal isOpen={open} onClose={clearAndToggle} isCentered>
30+
<ModalOverlay />
31+
<ModalContent p="10" minW="30%">
32+
<ModalCloseButton />
33+
<ModalHeader>
34+
<HStack>
35+
<Box>
36+
{event?.name} QR Code | {event?.key}
37+
</Box>
38+
<Box>
39+
<Switch
40+
size="lg"
41+
onChange={() => {
42+
handleToggleColor();
43+
}}
44+
defaultChecked
45+
sx={{
46+
'.chakra-switch__track': {
47+
bg: isToggled ? '#d4696a' : '#000000'
48+
}
49+
}}
50+
/>
51+
</Box>
52+
</HStack>
53+
</ModalHeader>
54+
<ModalBody>
55+
{event?.key ? (
56+
<Box>
57+
<EventQRCode
58+
eventKey={event?.key}
59+
size={256}
60+
color={isToggled ? '#d4696a' : '#000000'}
61+
/>
62+
</Box>
63+
) : (
64+
'Event key not found'
65+
)}
66+
</ModalBody>
67+
</ModalContent>
68+
</Modal>
69+
);
70+
};
71+
72+
export default QRCodeModal;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Event } from '../../../types/event';
2+
3+
export interface QRCodeModalProps {
4+
open: boolean;
5+
event?: Event;
6+
toggleModal: () => void;
7+
}

0 commit comments

Comments
 (0)