Skip to content

Commit

Permalink
Make the validator actually validate
Browse files Browse the repository at this point in the history
Previously it only checked if you had a user, and the qr did nothing at
all. With this change, we validate that the user is a member of Abakus
and the QR scanner also displays success or error messages for
validation status.
  • Loading branch information
LudvigHz committed Feb 16, 2023
1 parent 14226b4 commit 04406cf
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 47 deletions.
12 changes: 10 additions & 2 deletions app/actions/UserActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,15 @@ export function updatePicture({
);
};
}
export function fetchUser(username = 'me'): Thunk<any> {

const defaultOptions = {
propagateError: true,
};

export function fetchUser(
username = 'me',
{ propagateError } = defaultOptions
): Thunk<any> {
return callAPI({
types: User.FETCH,
endpoint: `/users/${username}/`,
Expand All @@ -276,7 +284,7 @@ export function fetchUser(username = 'me'): Thunk<any> {
errorMessage: 'Henting av bruker feilet',
isCurrentUser: username === 'me',
},
propagateError: true,
propagateError,
});
}
export function refreshToken(token: EncodedToken): Thunk<any> {
Expand Down
12 changes: 7 additions & 5 deletions app/components/Search/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ import { Keyboard } from 'app/utils/constants';
import type { Location } from 'history';
import type { ChangeEventHandler, KeyboardEvent } from 'react';

type Props = {
type Props<T> = {
searching: boolean;
location: Location;
inputRef?: {
current: HTMLInputElement | null | undefined;
};
onQueryChanged: (arg0: string) => void;
placeholder?: string;
results: Array<SearchResult>;
handleSelect: (arg0: SearchResult) => Promise<void>;
results: Array<T>;
handleSelect: (arg0: T) => Promise<void>;
};

const SearchPage = (props: Props) => {
const SearchPage = <SearchType extends SearchResult>(
props: Props<SearchType>
) => {
const [selectedIndex, setSelectedIndex] = useState<number>(0);
const [query, setQuery] = useState<unknown>(
qs.parse(props.location.search, {
Expand Down Expand Up @@ -59,7 +61,7 @@ const SearchPage = (props: Props) => {
}
};

const handleSelect = (result: SearchResult) => {
const handleSelect = (result: SearchType) => {
setQuery('');
setSelectedIndex(0);
props.handleSelect(result);
Expand Down
58 changes: 27 additions & 31 deletions app/components/UserValidator/index.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,58 @@
import cx from 'classnames';
import { get } from 'lodash';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import { QrReader } from 'react-qr-reader';
import goodSound from 'app/assets/good-sound.mp3';
import Button from 'app/components/Button';
import Icon from 'app/components/Icon';
import Modal from 'app/components/Modal';
import SearchPage from 'app/components/Search/SearchPage';
import type { SearchResult } from 'app/reducers/search';
import type { User } from 'app/models';
import type { UserSearchResult } from 'app/reducers/search';
import styles from './Validator.css';
import type { ComponentProps } from 'react';
import type { Required } from 'utility-types';

type UserWithUsername = Required<Partial<UserSearchResult>, 'username'>;

type Props = {
clearSearch: () => void;
handleSelect: (arg0: SearchResult) => Promise<void>;
location: Record<string, any>;
handleSelect: (arg0: UserWithUsername) => Promise<User>;
onQueryChanged: (arg0: string) => void;
results: Array<SearchResult>;
results: Array<UserSearchResult>;
searching: boolean;
};
} & ComponentProps<typeof SearchPage<UserSearchResult>>;

const Validator = (props: Props) => {
const { clearSearch, handleSelect } = props;
const input = useRef<HTMLInputElement | null | undefined>(null);
const [completed, setCompleted] = useState(false);
const [showScanner, setShowScanner] = useState(false);
const [scannerResult, setScannerResult] = useState('');

const showCompleted = () => {
setCompleted(true);
setTimeout(() => setCompleted(false), 2000);
};

const onSelect = useCallback(
(result: SearchResult) => {
(result: UserWithUsername) => {
clearSearch();
return handleSelect(result)
.then(
() => {
const sound = new window.Audio(goodSound);
sound.play();
showCompleted();
(user: User) => {
if (user.isAbakusMember) {
const sound = new window.Audio(goodSound);
sound.play();
showCompleted();
} else {
alert('Brukeren er ikke medlem av Abakus!');
}
},
(err) => {
const payload = get(err, 'payload.response.jsonData');

if (payload && payload.errorCode === 'not_registered') {
alert('Bruker er ikke påmeldt på eventet!');
} else if (payload && payload.errorCode === 'already_present') {
alert(payload.error);
if (payload && payload.detail === 'Not found.') {
alert(`Brukeren finnes ikke!\nBrukernavn: ${result.username}`);
} else {
alert(
`Det oppsto en uventet feil: ${JSON.stringify(payload || err)}`
Expand All @@ -63,22 +68,15 @@ const Validator = (props: Props) => {
},
[clearSearch, handleSelect]
);
useEffect(() => {

const handleScannerResult = (scannerResult: string) => {
if (scannerResult.length > 0 && !completed) {
onSelect({
username: scannerResult,
result: '',
color: '',
content: '',
icon: '',
label: '',
link: '',
path: '',
picture: '',
value: '',
});
}
}, [completed, onSelect, scannerResult]);
};

return (
<>
<div
Expand All @@ -103,14 +101,12 @@ const Validator = (props: Props) => {
<QrReader
onResult={(res, error) => {
if (res) {
setScannerResult(res.getText());
handleScannerResult(res.getText());
}

if (error) {
console.info(error);
}

setScannerResult('');
}}
constraints={{
facingMode: 'environment',
Expand All @@ -124,7 +120,7 @@ const Validator = (props: Props) => {
<Icon className={styles.qrIcon} name="qr-code" size={18} />
Vis scanner
</Button>
<SearchPage
<SearchPage<UserSearchResult>
{...props}
placeholder="Skriv inn brukernavn eller navn"
handleSelect={onSelect}
Expand Down
5 changes: 5 additions & 0 deletions app/declaration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ declare module '*.jpg' {
const value: string;
export default value;
}

declare module '*.mp3' {
const value: string;
export default value;
}
1 change: 1 addition & 0 deletions app/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export type User = {
memberships?: UserMembership[];
abakusEmailLists?: EmailList[];
permissionsPerGroup?: PermissionPerGroup[];
isAbakusMember?: boolean;
};

export type Penalty = {
Expand Down
3 changes: 2 additions & 1 deletion app/reducers/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ type SearchResultBase = {
profilePicture?: string;
};

type UserSearchResult = SearchResultBase & {
export type UserSearchResult = SearchResultBase & {
username: string;
profilePicture: string;
type: 'Bruker';
isAbakusMember: boolean;
};

export type SearchResult = SearchResultBase | UserSearchResult;
Expand Down
29 changes: 22 additions & 7 deletions app/routes/userValidator/ValidatorRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import qs from 'qs';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { autocomplete } from 'app/actions/SearchActions';
import { fetchUser } from 'app/actions/UserActions';
import { Content } from 'app/components/Content';
import Validator from 'app/components/UserValidator';
import type { User } from 'app/models';
import { selectAutocompleteRedux as selectAutocomplete } from 'app/reducers/search';
import type { UserSearchResult } from 'app/reducers/search';
import withPreparedDispatch from 'app/utils/withPreparedDispatch';
import type { ComponentProps } from 'react';

const searchTypes = ['users.user'];

const loadData = async (props, dispatch): any => {
const loadData = async (props, dispatch): Promise<void> => {
const query = qs.parse(props.location.search, {
ignoreQueryPrefix: true,
}).q;
Expand All @@ -34,13 +38,24 @@ const mapStateToProps = (state, props) => {
};
};

const mapDispatchToProps = (dispatch, { eventId }) => {
const url = `/validator?q=`;
const mapDispatchToProps = (dispatch, { location }) => {
const search = qs.parse(location.search, {
ignoreQueryPrefix: true,
});

return {
clearSearch: () => dispatch(push(url)),
handleSelect: () => Promise.resolve(),
clearSearch: () =>
dispatch(push(`/validator?${qs.stringify({ ...search, q: '' })}`)),

handleSelect: async (result: UserSearchResult): Promise<User> => {
const fetchRes = await dispatch(
fetchUser(result.username, { propagateError: false })
);

return Object.values(fetchRes.payload?.entities?.users)[0] as User;
},
onQueryChanged: debounce((query) => {
dispatch(push(url + query));
dispatch(push(`/validator?${qs.stringify({ ...search, q: query })}`));

if (query) {
dispatch(autocomplete(query, searchTypes));
Expand All @@ -49,7 +64,7 @@ const mapDispatchToProps = (dispatch, { eventId }) => {
};
};

const WrappedValidator = (props) => (
const WrappedValidator = (props: ComponentProps<typeof Validator>) => (
<Content>
<Validator {...props} />
</Content>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"test:coverage": "yarn run test -- --coverage",
"test:watch": "yarn run test --watch",
"lint": "yarn run lint:js && yarn run lint:css && yarn run lint:prettier",
"lint:js": "eslint . --ignore-path .prettierignore --max-warnings 1119",
"lint:js": "eslint . --ignore-path .prettierignore --max-warnings 1075",
"lint:css": "stylelint './app/**/*.css'",
"lint:prettier": "prettier '**/*.{ts,tsx,js,css,md,json}' --check",
"prettier": "prettier '**/*.{ts,tsx,js,css,md,json}' --write",
Expand Down

0 comments on commit 04406cf

Please sign in to comment.