diff --git a/src/App.jsx b/src/App.jsx
index 0a9f0f6..6983728 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -8,7 +8,7 @@ import { initializeApp } from 'firebase/app';
import { getAnalytics, logEvent } from 'firebase/analytics';
import { Disclaimer, Header, Sidebar, Section, ParcelInformation, ParcelTypeAhead, TypeAhead } from './PageElements';
import ParcelMap from './MapView';
-import { TailwindDartboard } from './vendor/Dartboard/Dartboard';
+import { TailwindDartboard } from '@ugrc/dart-board';
import { useOpenClosed } from '@ugrc/utilities/hooks';
import { useHash, useMapZooming, useGraphicManager } from './hooks';
import extents from './extents';
diff --git a/src/vendor/Dartboard/Dartboard.jsx b/src/vendor/Dartboard/Dartboard.jsx
deleted file mode 100644
index e98242f..0000000
--- a/src/vendor/Dartboard/Dartboard.jsx
+++ /dev/null
@@ -1,418 +0,0 @@
-import PropTypes from 'prop-types';
-import ky from 'ky';
-import classNames from 'clsx';
-import { useState, useCallback } from 'react';
-
-const ADDRESS_TYPE = 'single-address';
-const MILEPOST_TYPE = 'route-milepost';
-
-const defaultProps = {
- type: ADDRESS_TYPE,
- address: {
- acceptScore: 70,
- suggest: 0,
- locators: 'all',
- poBox: false,
- scoreDifference: false,
- },
- milepost: {
- side: 'increasing',
- fullRoute: false,
- },
- wkid: 3857,
- callback: null,
- format: null,
- pointSymbol: {
- style: 'diamond',
- color: [255, 0, 0, 0.5],
- },
- events: {
- success: console.log,
- error: console.error,
- },
-};
-
-const sanitize = (attributes = {}) => {
- const dartboardCustomProps = ['beforeClick', 'beforeKeyUp'];
-
- return Object.keys(attributes)
- .filter((key) => dartboardCustomProps.indexOf(key) === -1)
- .reduce((res, key) => ((res[key] = attributes[key]), res), {});
-};
-
-const BootstrapDartboard = (props) => {
- const {
- getFirstLabelProps,
- getSecondLabelProps,
- getFirstInputProps,
- getSecondInputProps,
- getButtonProps,
- isFirstInputValid,
- isSecondInputValid,
- found,
- } = useDartboard(props);
-
- return (
-
-
-
- {!isFirstInputValid ? This field is required : null}
-
-
-
- {!isSecondInputValid ? This field is required : null}
-
-
-
- {found === false ? No match found : null}
-
-
- );
-};
-
-const TailwindDartboard = (props) => {
- const {
- getFirstLabelProps,
- getSecondLabelProps,
- getFirstInputProps,
- getSecondInputProps,
- getButtonProps,
- getFirstHelpProps,
- getSecondHelpProps,
- isFirstInputValid,
- isSecondInputValid,
- found,
- } = useDartboard(props);
-
- return (
-
- {props.children}
-
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
-
-
- {!isFirstInputValid ? (
-
- ) : null}
-
-
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
-
-
- {!isSecondInputValid ? (
-
- ) : null}
-
-
-
- {(() => {
- if (found === false) {
- return No match found;
- } else if (found === true) {
- return (
-
-
- ✅
-
-
- );
- } else {
- return null;
- }
- })()}
-
-
- );
-};
-
-const useDartboard = (userProps = {}) => {
- const props = {
- ...defaultProps,
- ...userProps,
- };
-
- const [firstInput, setFirstInput] = useState();
- const [secondInput, setSecondInput] = useState();
- const [firstIsValid, setFirstIsValid] = useState(true);
- const [secondIsValid, setSecondIsValid] = useState(true);
- const [found, setFound] = useState();
-
- let baseUrl = 'https://api.mapserv.utah.gov/api/v1/geocode';
- if (props.type !== ADDRESS_TYPE) {
- baseUrl += '/milepost';
- }
-
- const getFirstLabelProps = (labelProps) => ({
- htmlFor: props.type === ADDRESS_TYPE ? 'dartboard_street_input' : 'dartboard_milepost_input',
- children: props.type === ADDRESS_TYPE ? 'Street address' : 'Route',
- ...labelProps,
- });
-
- const getSecondLabelProps = (labelProps) => ({
- htmlFor: props.type === ADDRESS_TYPE ? 'dartboard_zone_input' : 'dartboard_route_input',
- children: props.type === ADDRESS_TYPE ? 'City or Zip code' : 'Milepost',
- ...labelProps,
- });
-
- const getFirstInputProps = (inputProps) => ({
- onChange: (e) => setFirstInput(e.target.value),
- name: props.type === ADDRESS_TYPE ? 'dartboard_street_input' : 'dartboard_milepost_input',
- onKeyUp: (e) => {
- inputProps?.beforeKeyUp(e);
- handleKeyUp(e);
- },
- autoComplete: 'never',
- ...sanitize(inputProps),
- });
-
- const getSecondInputProps = (inputProps) => ({
- onChange: (e) => setSecondInput(e.target.value),
- name: props.type === ADDRESS_TYPE ? 'dartboard_zone_input' : 'dartboard_route_input',
- onKeyUp: (e) => {
- inputProps?.beforeKeyUp(e);
- handleKeyUp(e);
- },
- autoComplete: 'never',
- ...sanitize(inputProps),
- });
-
- const getFirstHelpProps = (inputProps) => ({
- children: props.type === ADDRESS_TYPE ? 'A street address is required' : 'A highway route number is required',
- ...inputProps,
- });
-
- const getSecondHelpProps = (inputProps) => ({
- children: props.type === ADDRESS_TYPE ? 'A city or zip code is required' : 'A milepost number is required',
- ...inputProps,
- });
-
- const getButtonProps = (buttonProps) => ({
- onClick: (e) => {
- buttonProps?.beforeClick(e);
- find(e);
- },
- type: 'button',
- ...sanitize(buttonProps),
- });
-
- const validate = useCallback(() => {
- const firstValidity = firstInput?.trim()?.length > 0;
- const secondValidity = secondInput?.trim()?.length > 0;
-
- setFirstIsValid(firstValidity);
- setSecondIsValid(secondValidity);
-
- // reset not found message
- setFound(null);
-
- return firstValidity && secondValidity;
- }, [firstInput, secondInput]);
-
- const get = useCallback(
- (options) => {
- const url = `${baseUrl}/${options.firstInput}/${options.secondInput}`;
-
- let query = {
- apiKey: props.apiKey,
- spatialReference: props.wkid,
- format: props.format,
- };
-
- if (props.callback) {
- query.callback = props.callback;
- }
-
- if (props.type === ADDRESS_TYPE) {
- query = { ...props.address, ...query };
- } else {
- query = { ...props.milepost, ...query };
- }
-
- return ky.get(url, {
- searchParams: query,
- mode: 'cors',
- });
- },
- [props.apiKey, props.wkid, props.address, props.milepost, props.type, props.format, props.callback, baseUrl]
- );
-
- const outputTransform = useCallback(
- (result, point) => {
- let attributes = {
- ...result.attributes,
- address:
- result.matchAddress ||
- result.attributes.MatchAddress ||
- result.inputAddress ||
- result.attributes.InputAddress,
- };
-
- let popupTemplate = {
- title: '{address}',
- };
-
- if (props.type !== ADDRESS_TYPE) {
- attributes = {
- matchRoute: result.matchRoute || result.attributes.MatchRoute,
- };
- popupTemplate = {
- title: '{matchRoute}',
- };
- }
-
- return {
- geometry: point,
- symbol: props.pointSymbol,
- attributes,
- popupTemplate,
- };
- },
- [props.pointSymbol, props.type]
- );
-
- const extractResponse = useCallback(
- async (response) => {
- if (!response.ok) {
- setFound(false);
-
- return props.events.error(await response.json());
- }
-
- let result = await response.json();
-
- if (result.status !== 200) {
- setFound(false);
-
- return props.events.error(response);
- }
-
- result = result.result;
-
- if (props.format?.toLowerCase() === 'geojson') {
- return result;
- }
-
- setFound(true);
-
- let point = {
- type: 'point',
- x: result?.location?.x,
- y: result?.location?.y,
- spatialReference: {
- wkid: props.wkid,
- },
- };
-
- if (props.format?.toLowerCase() === 'esrijson') {
- point.x = result?.geometry?.x;
- point.y = result?.geometry?.y;
- }
-
- return outputTransform(result, point);
- },
- [outputTransform, props.wkid, props.format, props.events]
- );
-
- const find = useCallback(async () => {
- if (!validate()) {
- return false;
- }
-
- let response;
-
- try {
- response = await get({
- firstInput,
- secondInput,
- });
- } catch (err) {
- return props.events.error(
- response?.text() || {
- message: err.message,
- status: 400,
- }
- );
- }
-
- const location = await extractResponse(response);
-
- if (location) {
- return props.events.success(location);
- }
- }, [firstInput, secondInput, validate, props.events, get, extractResponse]);
-
- const handleKeyUp = useCallback(
- (event) => {
- if (event.key !== 'Enter') {
- return;
- }
-
- find();
- },
- [find]
- );
-
- return {
- // prop getters
- getFirstLabelProps,
- getSecondLabelProps,
- getFirstInputProps,
- getSecondInputProps,
- getButtonProps,
- getFirstHelpProps,
- getSecondHelpProps,
- // actions
- setFirstIsValid,
- setSecondIsValid,
- setFound,
- // state
- isSecondInputValid: secondIsValid,
- isFirstInputValid: firstIsValid,
- found,
- firstInput,
- secondInput,
- };
-};
-
-BootstrapDartboard.propTypes = TailwindDartboard.propTypes = {
- apiKey: PropTypes.string.isRequired,
- type: PropTypes.oneOf([ADDRESS_TYPE, MILEPOST_TYPE]),
- pointSymbol: PropTypes.object,
- events: PropTypes.exact({
- success: PropTypes.func.isRequired,
- error: PropTypes.func.isRequired,
- }),
- wkid: PropTypes.number,
- address: PropTypes.shape({
- acceptScore: PropTypes.number,
- suggest: PropTypes.number,
- locators: PropTypes.oneOf([null, 'all', 'addressPoints', 'roadCenterlines']),
- poBox: PropTypes.bool,
- scoreDifference: PropTypes.bool,
- }),
- milepost: PropTypes.shape({
- side: PropTypes.oneOf([null, 'increasing', 'decreasing']),
- fullRoute: PropTypes.bool,
- }),
- format: PropTypes.oneOf([null, 'esrijson', 'geojson']),
- callback: PropTypes.string,
-};
-
-export { useDartboard, BootstrapDartboard, TailwindDartboard };