Icon.APPLY = makePreset('ApplyIcon', 'right-to-bracket');
Icon.APPLICATIONS = makePreset('ApplicationsIcon', 'person-running');
Icon.CHART = makePreset('ChartIcon', 'chart-line');
+Icon.CROSSHAIRS = makePreset('CrosshairsIcon', 'crosshairs');
Icon.DISCORD = makePreset('DiscordIcon', ['fab', 'discord']);
Icon.EDIT = makePreset('EditIcon', 'edit');
Icon.FINISHED = makePreset('FinishedIcon', 'square-check');
--- /dev/null
+import OpenSeadragon from 'openseadragon';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+import { Link } from 'react-router-dom';
+
+import { useOpenSeadragon } from './OpenSeadragon';
+import Icon from '../common/Icon';
+import Rulesets from '../techniques/Rulesets';
+import {
+ getLink,
+ getRelations,
+ getTranslation,
+ hasRelations,
+ sorted,
+} from '../../helpers/Technique';
+import i18n from '../../i18n';
+
+const Item = ({ pin }) => {
+ const { viewer } = useOpenSeadragon();
+ const { t } = useTranslation();
+
+ const goToLocation = React.useCallback(pin => {
+ if (viewer && viewer.viewport) {
+ viewer.viewport.panTo(new OpenSeadragon.Point(pin.x, pin.y));
+ viewer.viewport.zoomTo(4);
+ viewer.element.scrollIntoView();
+ }
+ }, [viewer]);
+
+ return <li className="d-flex align-items-start justify-content-between">
+ <div className="flex-grow-1">
+ {pin.technique.type === 'location' ? <>
+ <h2>{getTranslation(pin.technique, 'title', i18n.language)}</h2>
+ <p>{getTranslation(pin.technique, 'short', i18n.language)}</p>
+ {hasRelations(pin.technique, 'related') ?
+ sorted(getRelations(pin.technique, 'related')).map(r =>
+ <div
+ className="d-flex align-items-start justify-content-between"
+ key={r.id}
+ >
+ <div className="me-auto">
+ <h3>
+ <Link to={getLink(r)}>
+ {getTranslation(r, 'title', i18n.language)}
+ </Link>
+ </h3>
+ <p>{getTranslation(r, 'short', i18n.language)}</p>
+ </div>
+ {r.rulesets ?
+ <Rulesets technique={r} />
+ : null}
+ </div>
+ )
+ : null}
+ </> : <div className="d-flex align-items-start justify-content-between">
+ <div className="flex-grow-1">
+ <h2>
+ <Link to={getLink(pin.technique)}>
+ {getTranslation(pin.technique, 'title', i18n.language)}
+ </Link>
+ </h2>
+ <p>{getTranslation(pin.technique, 'short', i18n.language)}</p>
+ </div>
+ {pin.technique.rulesets ?
+ <Rulesets technique={pin.technique} />
+ : null}
+ </div>}
+ </div>
+ <Button
+ className="m-2"
+ onClick={() => goToLocation(pin)}
+ title={t('map.goToLocation')}
+ variant="outline-secondary"
+ >
+ <Icon.CROSSHAIRS title="" />
+ </Button>
+ </li>;
+};
+
+Item.propTypes = {
+ pin: PropTypes.shape({
+ technique: PropTypes.shape({
+ rulesets: PropTypes.shape({
+ }),
+ type: PropTypes.string,
+ }),
+ x: PropTypes.number,
+ y: PropTypes.number,
+ }),
+};
+
+export default Item;
--- /dev/null
+import React from 'react';
+import { Container } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import Item from './Item';
+import { useOpenSeadragon } from './OpenSeadragon';
+import { compareTranslation } from '../../helpers/Technique';
+import i18n from '../../i18n';
+
+const List = () => {
+ const { pins } = useOpenSeadragon();
+ const { t } = useTranslation();
+
+ const sortedPins = React.useMemo(() => {
+ const compare = compareTranslation('title', i18n.language);
+ return pins.sort((a, b) => compare(a.technique, b.technique));
+ }, [pins, i18n.language]);
+
+ if (!pins || !pins.length) return null;
+
+ return <Container className="mt-3">
+ <h2>{t('map.onThisMap')}</h2>
+ <ul className="pin-list">
+ {sortedPins.map(pin =>
+ <Item key={pin.id} pin={pin} />
+ )}
+ </ul>
+ </Container>;
+};
+
+export default List;