Skip to content

Instantly share code, notes, and snippets.

@14paxton
Last active January 14, 2024 16:37
Show Gist options
  • Save 14paxton/09adce350289bdcc1df92ed425c1d548 to your computer and use it in GitHub Desktop.
Save 14paxton/09adce350289bdcc1df92ed425c1d548 to your computer and use it in GitHub Desktop.
React Typescript Keyboard input reader
import { ReactNode, useEffect } from 'react';
import useBarcodeScanner from './useBarcodeScanner';
import { NavigateFunction, useNavigate } from 'react-router-dom';
import { ScanCacDetails } from './Scan';
import { GuestAction } from '../../state/GuestContext';
const isEmpty = (obj: object): boolean => Object.keys(obj).length === 0;
const BarCode = ({ children }: { children: ReactNode }) => {
const navigate: NavigateFunction = useNavigate();
const [cacDetails, guestContextValues]: [ScanCacDetails, GuestAction] = useBarcodeScanner();
const handleScannedDetails = function () {
if (!isEmpty(guestContextValues) && !cacDetails.scanError) {
console.debug('CAC Barcode Parsed, Navigate to stay page, GuestContextValues: ', guestContextValues);
navigate('stay');
}
const handleUnmounting = function () {
console.debug('Barcode Component Mount/Un-Mount, guestContextValues: ', guestContextValues);
console.debug('Barcode Component Mount/Un-Mount, cacDetails: ', cacDetails);
};
return handleUnmounting();
};
useEffect(handleScannedDetails, [cacDetails, guestContextValues, navigate]);
return children;
};
export default BarCode;
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
import { buildNewScanEvent, ScanCacDetails, ScanEvent } from './Scan';
import parse from '../commonAccessCard/Parse';
export const defaultOptions: BarcodeScannerOptions = {
timeOut: 137, characterCount: 18,
};
export function isShiftKey({ shiftKey, key }: KeyboardEvent): string {
const shift: boolean = shiftKey || 'Shift' === key;
if (shift) {
return key.replace('Shift', '');
} else {return key;}
}
export type BarcodeScannerOptions = {
timeOut: number; characterCount: number;
};
interface BarcodeEventMap {
scan: ScanEvent;
}
export interface BarcodeScanner extends EventTarget {
addEventListener<K extends keyof BarcodeEventMap>(type: K, listener: (this: BarcodeScanner, ev: BarcodeEventMap[K]) => void, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof BarcodeEventMap>(type: K, listener: (this: BarcodeScanner, ev: BarcodeEventMap[K]) => void, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
export class BarcodeScanner extends EventTarget {
capture: string;
target: EventTarget;
private timer: number;
private readonly timeOut: number;
private readonly characterCount: number;
private scanCacDetails: ScanCacDetails;
private scanReadComplete: boolean = false;
private _handleCompletedScan: (scanCacDetails: (ScanCacDetails | object)) => void = (scanCacDetails: ScanCacDetails | object): void => {
this.scanCacDetails = { ...this.scanCacDetails, ...scanCacDetails };
this.dispatchScanEvent();
};
protected get handleCompletedScan(): (scanCacDetails: (ScanCacDetails | object)) => void {
return this._handleCompletedScan;
}
private _reset: () => void = (): void => {
console.debug('BarcodeScanner Reset Values. Ending Values: ', {
timer: this.timer, capture: this.capture, scanCacDetails: this.scanCacDetails,
});
this.timer = performance.now();
this.capture = '';
this.scanCacDetails = <ScanCacDetails>{ scanError: false };
this.scanReadComplete = false;
};
protected get reset(): () => void {
return this._reset;
}
constructor(userDefinedOptions: BarcodeScannerOptions) {
super();
const options: BarcodeScannerOptions = <BarcodeScannerOptions>{ ...defaultOptions, ...userDefinedOptions };
this.timeOut = options.timeOut;
this.characterCount = options.characterCount;
this.scanCacDetails = <ScanCacDetails>{ scanError: false };
this.timer = performance.now();
this.capture = '';
this.target = new EventTarget();
window.addEventListener('keyup', this.keyup.bind(this));
}
keyup(event: KeyboardEvent): void {
// Set current time
const now: number = performance.now();
// If out timer is out, we need to reset because it was not a barcode
if (now - this.timer > this.timeOut) {
this._reset();
}
// It seems we are still fast enough to be a barcode, so add to capture
const sinceFirst: number = now - this.timer;
if (sinceFirst < this.timeOut) {
this.capture += isShiftKey(event).toUpperCase();
// It seems we managed to get enough characters within the time-out, send scan!
if (this.capture.length === this.characterCount && !this.scanReadComplete) {
console.debug('BarcodeScanner CAC Scanner Scanned Code: ', this.capture);
this.scanReadComplete = true;
parse(this.capture).then(this._handleCompletedScan);
}
}
}
dispatchScanEvent(): void {
console.debug('BarcodeScanner Dispatch Scan Event CAC Details: ', this.scanCacDetails);
const event: ScanEvent = buildNewScanEvent(this.scanCacDetails);
this.dispatchEvent(event);
this._reset();
}
}
export default function buildNewBarcodeScanner(options?: BarcodeScannerOptions): BarcodeScanner {
return new BarcodeScanner(options || defaultOptions);
}
export enum CACMatchParts {
BarcodeVersion = 'barcodeVersion',
PersonalDesignatorIdentifier = 'personalDesignatorIdentifier',
PersonalDesignatorType = 'personalDesignatorType',
EDIPI = 'EDIPI',
FirstName = 'firstName',
LastName = 'lastName',
DateofBirth = 'dateofBirth',
PersonnelCategoryCode = 'personnelCategoryCode',
BranchCode = 'branchCode',
PersonnelEntitlementConditionType = 'personnelEntitlementConditionType',
Rank = 'rank',
PayPlanCode = 'payPlanCode',
PayPlanGradeCode = 'payPlanGradeCode',
CardIssueDate = 'cardIssueDate',
CardExpirationDate = 'cardExpirationDate',
CardInstanceIdentifier = 'cardInstanceIdentifier',
MiddleInitial = 'middleInitial',
Nationality = 'nationality',
}
type CACMatchNumberParts =
| CACMatchParts.PersonalDesignatorIdentifier
| CACMatchParts.EDIPI;
type CACMatchDateParts =
| CACMatchParts.DateofBirth
| CACMatchParts.CardIssueDate
| CACMatchParts.CardExpirationDate;
export type CACDetails = {
[key in CACMatchParts]: string;
} & {
[key in CACMatchNumberParts]: number;
} & {
[key in CACMatchDateParts]: Date;
} & {
personnelCategory: string;
branch: string;
branchID: string;
rankID: string;
};
export interface CACResult {
cac: CACDetails;
match: RegExpMatchArray;
}
export enum PersonnelCategories {
'Active Duty',
'Retired',
'Foreign National',
'NAF',
'Reserve',
'Contractor',
'Guard',
'Other',
}
export enum PersonnelCategoryCodes {
A = PersonnelCategories['Active Duty'],
R = PersonnelCategories.Retired,
Y = PersonnelCategories.Retired,
U = PersonnelCategories['Foreign National'],
K = PersonnelCategories.NAF,
V = PersonnelCategories.Reserve,
S = PersonnelCategories.Reserve,
E = PersonnelCategories.Contractor,
O = PersonnelCategories.Contractor,
N = PersonnelCategories.Guard,
}
export type Branch = {
A: string;
C: string;
D: string;
F: string;
H: string;
M: string;
N: string;
0: string;
1: string;
2: string;
3: string;
4: string;
X: string;
};
export const Branches: Branch = {
A: 'Army',
C: 'Coast Guard',
D: 'Civilian',
F: 'Air Force',
H: 'Public Health Services',
M: 'USMC',
N: 'Navy',
0: 'National Oceanic and Atmospheric Administration',
1: 'Foreign Army',
2: 'Foreign Navy',
3: 'Foreign Marine Corps',
4: 'Foreign Air Force',
X: 'Other',
};
export enum ScanType {
cac = 'cac',
cac39 = 'cac39',
loa = 'loa',
}
export const CACEpoch = new Date(Date.parse('1/1/1000'));
enum GuestActionType {
CLEAR = 'CLEAR',
MANUAL_ENTRY = 'MANUAL_ENTRY',
BARCODE_SCAN = 'BARCODE_SCAN',
}
export default GuestActionType;
import React, { Dispatch, ReactNode, useReducer } from 'react';
export type GuestState = {
dodId?: string; lastName?: string;
};
export type GuestContextValues = {
state: GuestState; dispatch: Dispatch<GuestAction>;
};
export const GuestContext = React.createContext<GuestContextValues | undefined>(undefined);
export type GuestAction = {
type: 'CLEAR' | 'MANUAL_ENTRY' | 'BARCODE_SCAN'; payload?: {
dodId: string; lastName?: string;
};
};
const reducer = (state: GuestState, action: GuestAction) => {
switch (action.type) {
case 'CLEAR':
return {};
case 'MANUAL_ENTRY':
return {
dodId: action.payload?.dodId, lastName: action.payload?.lastName,
};
case 'BARCODE_SCAN':
return {
dodId: action.payload?.dodId, lastName: undefined,
};
default:
return state;
}
};
export const GuestContextProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(reducer, {});
return (<GuestContext.Provider value={{ state, dispatch }}>
{children}
</GuestContext.Provider>);
};
import { Dispatch } from 'react';
export type GuestState = {
dodId?: string;
lastName?: string;
};
export type GuestAction = {
type: 'CLEAR' | 'MANUAL_ENTRY' | 'BARCODE_SCAN';
payload?: {
dodId: string;
lastName?: string;
};
};
export type GuestContextValues = {
state: GuestState;
dispatch: Dispatch<GuestAction>;
};
export const reducer = (state: GuestState, action: GuestAction) => {
switch (action.type) {
case 'CLEAR':
return {};
case 'MANUAL_ENTRY':
return {
dodId: action.payload?.dodId,
lastName: action.payload?.lastName,
};
case 'BARCODE_SCAN':
return {
dodId: action.payload?.dodId,
lastName: undefined,
};
default:
return state;
}
};
import { Branch, Branches, CACDetails, PersonnelCategories, PersonnelCategoryCodes, ScanType } from './CacCommon';
import { ScanCacDetails } from '../barcodeScanner/Scan';
import GuestActionType from '../enum/GuestActionType';
enum CACMatchParts {
BarcodeVersion = 'barcodeVersion',
PersonalDesignatorIdentifier = 'personalDesignatorIdentifier',
PersonalDesignatorType = 'personalDesignatorType',
EDIPI = 'EDIPI',
PersonnelCategoryCode = 'personnelCategoryCode',
BranchCode = 'branchCode',
CardInstanceIdentifier = 'cardInstanceIdentifier',
}
const CAC39Match: RegExp = /([1N])([A-Z0-9]{6})([A-Z0-9])([A-Z0-9]{7})(.)(.)([A-Z0-9])/;
const CAC39MatchPartIndexes: CACMatchParts[] = Object.values<CACMatchParts>(CACMatchParts);
function getTypedKeys<T extends object>(o: T): (keyof T)[] {
return Object.keys(o) as (keyof T)[];
}
function getBranch(key: PropertyKey): string | undefined {
function getBranchByKey(k: keyof Branch): boolean { return k === key; }
const typedKey: keyof Branch | undefined = getTypedKeys(Branches).find(getBranchByKey);
if (typedKey) {
return Branches[typedKey];
} else {return undefined;}
}
interface CACPart {
part: string;
index: number;
indexes: CACMatchParts[];
}
export const parseCACPart = ({
part, index, indexes,
}: CACPart): Partial<CACDetails> => {
const key: CACMatchParts = indexes[index];
switch (key) {
case CACMatchParts.PersonalDesignatorIdentifier:
case CACMatchParts.EDIPI:
return {
[key]: parseInt(part, 32),
};
case CACMatchParts.PersonnelCategoryCode:
return {
personnelCategory: PersonnelCategories[PersonnelCategoryCodes[part as keyof typeof PersonnelCategoryCodes]], [key]: part,
};
case CACMatchParts.BranchCode:
return {
branch: getBranch(part), [key]: part,
};
default:
return {
[key]: part.trimEnd(),
};
}
};
function buildCacDetails(parsed: ScanCacDetails, part: string, index: number) {
return {
...parsed, ...parseCACPart({
part: part, index: index, indexes: CAC39MatchPartIndexes,
}),
};
}
export default async function parse(input: string): Promise<ScanCacDetails | object> {
const match: RegExpExecArray | null = CAC39Match.exec(input);
const cacDetailsObject: ScanCacDetails = <ScanCacDetails>{
scanType: GuestActionType.BARCODE_SCAN, cacScanType: ScanType.cac39,
};
let returnPromise: Promise<ScanCacDetails | object> = Promise.resolve({
scanError: true,
});
if (match) {
returnPromise = Promise.resolve(match
.slice(1)
.reduce(buildCacDetails, cacDetailsObject));
}
return returnPromise;
}
import { CACDetails, ScanType } from '../commonAccessCard/CacCommon';
import GuestActionType from '../enum/GuestActionType';
export interface ScanCacDetails extends CACDetails {
//Property EDIPI from CACDetails is DOD ID
scanType: GuestActionType.BARCODE_SCAN;
cacScanType: ScanType.cac39;
scanError: boolean;
}
export class ScanEvent extends CustomEvent<ScanCacDetails> {
constructor(detail: ScanCacDetails) {
super('scan', { detail });
}
}
export function buildNewScanEvent(detail: ScanCacDetails): ScanEvent {
return new ScanEvent(detail);
}
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { ScanCacDetails, ScanEvent } from './Scan';
import buildNewBarcodeScanner, { BarcodeScanner } from './BarcodeScanner';
import { GuestAction, GuestContext, GuestContextValues } from '../../state/GuestContext';
import GuestActionType from '../enum/GuestActionType';
export default function useBarcodeScanner(): [ScanCacDetails, GuestAction] {
const barcodeScanner: BarcodeScanner = useMemo(buildNewBarcodeScanner, []);
const { state: guestAction, dispatch }: GuestContextValues = useContext(GuestContext) as GuestContextValues;
const [cacDetails, setCacDetails]: [ScanCacDetails, (value: (((prevState: ScanCacDetails) => ScanCacDetails) | ScanCacDetails)) => void] = useState<ScanCacDetails>(<ScanCacDetails>{ scanError: false });
const setDetailsAndGuestContext = (event: ScanEvent): void => {
const { detail: details }: ScanEvent = event;
setCacDetails(details);
dispatch({
type: GuestActionType.BARCODE_SCAN, payload: {
dodId: details.EDIPI, lastName: details?.lastName,
},
} as GuestAction);
};
const handleEvent = useCallback(setDetailsAndGuestContext, [dispatch]);
const setAndRemoveEventListener = (): () => void => {
let createScanEventListener: boolean = true;
const removeEventListener = (): void => {
barcodeScanner.removeEventListener('scan', handleEvent);
createScanEventListener = false;
};
if (barcodeScanner && createScanEventListener) {
barcodeScanner.addEventListener('scan', handleEvent);
}
return removeEventListener;
};
useEffect(setAndRemoveEventListener, [barcodeScanner, handleEvent]);
return [<ScanCacDetails>cacDetails, <GuestAction>guestAction];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment