<div data-react="find-installer" data-props="{&quot;placesEndpoint&quot;:&quot;/mocks/api/findInstaller.json&quot;,&quot;lang&quot;:{&quot;startTitle&quot;:&quot;Hitta installatör&quot;,&quot;startDescription&quot;:&quot;Behöver du hjälp att installera? Eller är du kanske osäker på vilka produkter och lösningar som är bäst för dina behov i hemmet? Tala med en erfaren auktoriserad installatör så får du hjälp att skapa inomhusklimat i världsklass i ditt eget hem.&quot;,&quot;searchLabel&quot;:&quot;Sök på adress&quot;,&quot;geolocation&quot;:&quot;Använd min position&quot;,&quot;filterToggle&quot;:&quot;Visa filter&quot;,&quot;filterTitle&quot;:&quot;Välj filter&quot;,&quot;filterPlaceholder&quot;:&quot;Filter&quot;,&quot;filterRemove&quot;:&quot;Ta bort&quot;}}" class="find-installer"></div>
<div data-react="find-installer" data-props="{{jsonEncode props}}" class="find-installer"></div>
{
  "props": {
    "placesEndpoint": "/mocks/api/findInstaller.json",
    "lang": {
      "startTitle": "Hitta installatör",
      "startDescription": "Behöver du hjälp att installera? Eller är du kanske osäker på vilka produkter och lösningar som är bäst för dina behov i hemmet? Tala med en erfaren auktoriserad installatör så får du hjälp att skapa inomhusklimat i världsklass i ditt eget hem.",
      "searchLabel": "Sök på adress",
      "geolocation": "Använd min position",
      "filterToggle": "Visa filter",
      "filterTitle": "Välj filter",
      "filterPlaceholder": "Filter",
      "filterRemove": "Ta bort"
    }
  }
}
  • Content:
    import React from 'react';
    import PropTypes from 'prop-types';
    
    const AreaFilter = (props) => {
        const [isOpen, setIsOpen] = React.useState(false);
        const filterRef = React.useRef();
        const togglerRef = React.useRef();
        const optionsRef = React.useRef();
        const focusIndexRef = React.useRef(null);
        // Selected areas are shown as pills and removed from the dropdown
        const unselectedAreas = props.areas.filter((area) => !props.selectedAreas.includes(area));
    
        const onToggle = (area) => {
            props.onChange(
                props.selectedAreas.includes(area)
                    ? props.selectedAreas.filter((selectedArea) => selectedArea !== area)
                    : [...props.selectedAreas, area]
            );
        };
    
        const onSelectOption = (area, index) => {
            focusIndexRef.current = index;
            onToggle(area);
        };
    
        // Selected option disappears from the list, so keep focus in the dropdown
        React.useEffect(() => {
            if (focusIndexRef.current === null) {
                return;
            }
            const options = optionsRef.current.querySelectorAll('.select-multiple__option');
            const nextOption = options[Math.min(focusIndexRef.current, options.length - 1)];
            (nextOption || togglerRef.current).focus();
            focusIndexRef.current = null;
        }, [props.selectedAreas]);
    
        const close = () => {
            setIsOpen(false);
            togglerRef.current.focus();
        };
    
        // Close dropdown on click outside
        React.useEffect(() => {
            if (!isOpen) {
                return;
            }
            const onClickOutside = (e) => {
                if (!filterRef.current.contains(e.target)) {
                    setIsOpen(false);
                }
            };
            document.addEventListener('mousedown', onClickOutside);
    
            return () => document.removeEventListener('mousedown', onClickOutside);
        }, [isOpen]);
    
        return (
            <div
                className="find-installer-filter"
                id={props.id}
                ref={filterRef}
                onKeyDown={(e) => {
                    if (isOpen && (e.key === 'Escape' || e.key === 'Esc')) {
                        close();
                    }
                }}
            >
                {props.title &&
                    <div className="find-installer-filter__label" id={`${props.id}-label`}>{props.title}</div>
                }
                <div className="select-multiple">
                    <div className="select-multiple__wrapper">
                        {props.selectedAreas.map((area) => (
                            <button
                                type="button"
                                className="select-multiple__pill"
                                key={area}
                                aria-label={`${props.removeLabel} ${area}`}
                                onClick={() => onToggle(area)}
                            >
                                {area}
                            </button>
                        ))}
                        <button
                            type="button"
                            className="select-multiple__toggler"
                            ref={togglerRef}
                            aria-expanded={isOpen}
                            aria-controls={`${props.id}-options`}
                            aria-labelledby={props.title ? `${props.id}-label` : undefined}
                            onClick={() => setIsOpen((open) => !open)}
                        >
                            {props.placeholder}
                        </button>
                    </div>
                    <ul
                        className={`select-multiple__options ${isOpen ? 'select-multiple__options--open' : ''}`}
                        id={`${props.id}-options`}
                        ref={optionsRef}
                        hidden={!isOpen || !unselectedAreas.length}
                    >
                        {unselectedAreas.map((area, index) => (
                            <li key={area}>
                                <button
                                    type="button"
                                    className="select-multiple__option"
                                    onClick={() => onSelectOption(area, index)}
                                >
                                    {area}
                                </button>
                            </li>
                        ))}
                    </ul>
                </div>
            </div>
        );
    };
    
    AreaFilter.propTypes = {
        id: PropTypes.string,
        title: PropTypes.string,
        placeholder: PropTypes.string,
        removeLabel: PropTypes.string,
        areas: PropTypes.arrayOf(PropTypes.string).isRequired,
        selectedAreas: PropTypes.arrayOf(PropTypes.string).isRequired,
        onChange: PropTypes.func.isRequired
    };
    
    AreaFilter.defaultProps = {
        id: 'find-installer-filter',
        placeholder: 'Filter',
        removeLabel: 'Remove'
    };
    
    export default AreaFilter;
    
  • URL: /components/raw/find-installer/AreaFilter.jsx
  • Filesystem Path: src/components/find-installer/AreaFilter.jsx
  • Size: 4.8 KB
  • Content:
    import React from 'react';
    import PropTypes from 'prop-types';
    import StartView from './StartView';
    import MapView from './MapView';
    
    const FindInstaller = (props) => {
        const [selectedLocationCords, setSelectedLocationCords] = React.useState();
    
        const onSelect = (cords) => setSelectedLocationCords(() => cords);
    
        return (
            <>
                {selectedLocationCords
                    ? <MapView
                        selectedLocationCords={selectedLocationCords}
                        placesEndpoint={props.placesEndpoint}
                        lang={props.lang}
                    />
                    : <StartView
                        lang={props.lang}
                        onSelect={onSelect}
                    />
                }
            </>
        );
    };
    
    FindInstaller.propTypes = {
        placesEndpoint: PropTypes.string.isRequired
    };
    
    export default FindInstaller;
    
  • URL: /components/raw/find-installer/FindInstaller.jsx
  • Filesystem Path: src/components/find-installer/FindInstaller.jsx
  • Size: 862 Bytes
  • Content:
    import React from 'react';
    import PropTypes from 'prop-types';
    import GoogleMap from '../google-map/GoogleMap';
    import SearchFieldPlaces from './SearchFieldPlaces';
    import MapContactCard from '../map-contact-card/MapContactCard';
    import AreaFilter from './AreaFilter';
    import { installersToContactInfoProps } from './utils/parsers';
    import { getAreas, filterInstallersByAreas, getAreasFromUrl, setAreasInUrl } from './utils/areaFilter';
    
    const MapView = (props) => {
        const [selectedLocationCords, setSelectedLocationCords] = React.useState(props.selectedLocationCords);
        const [places, setPlaces] = React.useState();
        const [visiblePlaces, setVisiblePlaces] = React.useState([]);
        const [selectedPlace, setSelectedPlace] = React.useState('');
        const [selectedAreas, setSelectedAreas] = React.useState(getAreasFromUrl);
        const [isFilterOpen, setIsFilterOpen] = React.useState(selectedAreas.length > 0);
        const mapRef = React.useRef();
    
        const areas = React.useMemo(() => (places ? getAreas(places) : []), [places]);
        const filteredPlaces = React.useMemo(() => (
            places ? filterInstallersByAreas(places, selectedAreas) : places
        ), [places, selectedAreas]);
    
        const onAreasChange = (newAreas) => {
            setSelectedAreas(newAreas);
            setAreasInUrl(newAreas);
        };
    
        const openInfoWindow = (id) => {
            mapRef.current.openInfoWindow(id);
        };
    
        React.useEffect(() => {
            if (mapRef.current) {
                openInfoWindow(selectedPlace);
            }
        }, [selectedPlace, mapRef]);
    
        React.useEffect(() => {
            fetch(props.placesEndpoint)
                .then((res) => res.json())
                .then((data) => setPlaces(data.Result));
        }, []);
    
        return (
            <div className="find-installer-map">
                <aside className="find-installer-map__result">
                    <div className="row">
                        <div className="col col--span-12">
                            <div className="find-installer-map__search">
                                <SearchFieldPlaces
                                    onSelect={(cords) => setSelectedLocationCords(cords)}
                                    label={props.lang.searchLabel}
                                    geolocationLabel={props.lang.geolocation}
                                >
                                    {areas.length > 0 &&
                                        <button
                                            type="button"
                                            className={`button button--ghost search-field__icon-button ${isFilterOpen ? 'search-field__icon-button--active' : ''}`}
                                            aria-label={props.lang.filterToggle}
                                            aria-expanded={isFilterOpen}
                                            aria-controls="find-installer-filter"
                                            onClick={() => setIsFilterOpen((open) => !open)}
                                        >
                                            <span className="search-field__icon search-field__icon--filter" aria-hidden="true" />
                                        </button>
                                    }
                                </SearchFieldPlaces>
                            </div>
                            {areas.length > 0 && isFilterOpen &&
                                <AreaFilter
                                    id="find-installer-filter"
                                    title={props.lang.filterTitle}
                                    placeholder={props.lang.filterPlaceholder}
                                    removeLabel={props.lang.filterRemove}
                                    areas={areas}
                                    selectedAreas={selectedAreas}
                                    onChange={onAreasChange}
                                />
                            }
                        </div>
                    </div>
                    <div className="find-installer-map__cards">
                        <div className="row">
                            {filteredPlaces
                                ? installersToContactInfoProps(filteredPlaces, selectedLocationCords).map(({ contactInfo, key, title }) => (
                                    visiblePlaces.includes(key) &&
                                    <div className="col col--span-12 col--span-m-6 col--span-xl-12" key={key}>
                                        <MapContactCard
                                            key={key}
                                            id={String(key)}
                                            title={title}
                                            contactInfo={contactInfo}
                                            active={selectedPlace === key}
                                            onClick={() => setSelectedPlace(key)}
                                        />
                                    </div>
                                ))
                                // ToDo: Add loading state
                                : null}
                        </div>
                    </div>
                </aside>
                <div className="find-installer-map__map">
                    {filteredPlaces
                        ? <GoogleMap
                            ref={mapRef}
                            places={filteredPlaces}
                            targetPlaceCords={selectedLocationCords}
                            onVisibleMarkersUpdate={setVisiblePlaces}
                            onInfoWindowClose={() => setSelectedPlace(() => '')}
                            onMarkerClick={(marker) => setSelectedPlace(() => marker.Id)}
                        />
                        // ToDo: Add loading state
                        : null}
                </div>
            </div>
        );
    };
    
    MapView.propTypes = {
        selectedLocationCords: PropTypes.object.isRequired,
        setSearchLatLon: PropTypes.shape({
            lat: PropTypes.number,
            lon: PropTypes.number
        }),
        placesEndpoint: PropTypes.string.isRequired,
        lang: PropTypes.shape({
            searchLabel: PropTypes.string,
            geolocation: PropTypes.string,
            filterToggle: PropTypes.string,
            filterTitle: PropTypes.string,
            filterPlaceholder: PropTypes.string,
            filterRemove: PropTypes.string
        })
    };
    
    MapView.defaultProps = {
        lang: {}
    };
    
    export default MapView;
    
  • URL: /components/raw/find-installer/MapView.jsx
  • Filesystem Path: src/components/find-installer/MapView.jsx
  • Size: 6.3 KB
  • Content:
    import React from 'react';
    import PropTypes from 'prop-types';
    import { GoogleMapsContext } from '../../context/GoogleMapsProvider';
    import SearchField, { Geolocation } from '../search-field';
    
    const ItemComponent = (props) => (
        <>
            <svg className="search-field-places__icon-wrapper" focusable="false">
                <use xlinkHref="#icon-map-pin"/>
            </svg>
            {props.description}
        </>
    );
    
    const SearchFieldPlaces = (props) => {
        const { autocompleteService, googleMapsApi } = React.useContext(GoogleMapsContext);
        const [searchResult, setSearchResult] = React.useState([]);
    
        const updateSearchResult = (e) => {
            const input = e.target.value || '';
            if (input.length > 0) {
                autocompleteService.getPlacePredictions({ input }, (places) => {
                    setSearchResult(() => (
                        places
                            ? places.map((place) => ({
                                ...place,
                                value: place.description,
                                id: place.place_id
                            }))
                            : []
                    ));
                });
            } else {
                // Reset statae when there's no search result
                setSearchResult(() => []);
            }
        };
    
        const getCords = async (e) => {
            const service = new googleMapsApi.places.PlacesService(document.createElement('div'));
            const cords = await new Promise((resolve, reject) => {
                try {
                    service.getDetails({ placeId: e.place_id }, (data) => {
                        resolve(data.geometry.location);
                    });
                } catch (e) {
                    reject(e);
                }
            });
            props.onSelect(cords);
        };
    
        const onGeolocation = (latLng) => {
            props.onSelect(new googleMapsApi.LatLng(...latLng));
        };
    
        return (
            <>
                {googleMapsApi && props.label &&
                    <label id={`${props.inputId}-label`} htmlFor={props.inputId} className="form-item__label">
                        {props.label}
                    </label>
                }
                {googleMapsApi &&
                    <SearchField
                        inputId={props.inputId}
                        labelId={props.label ? `${props.inputId}-label` : undefined}
                        onChange={getCords}
                        items={searchResult}
                        onInputChange={updateSearchResult}
                        itemStyle="search-field-places__result"
                        className={props.className}
                        itemComponent={ItemComponent}
                    >
                        <Geolocation onGeolocation={onGeolocation} label={props.geolocationLabel} />
                        {props.children}
                    </SearchField>
                }
            </>
        );
    };
    
    SearchFieldPlaces.propTypes = {
        onChange: PropTypes.func,
        label: PropTypes.string,
        inputId: PropTypes.string,
        geolocationLabel: PropTypes.string
    };
    
    SearchFieldPlaces.defaultProps = {
        onChange: () => {},
        inputId: 'search-field-places-input'
    };
    
    export default SearchFieldPlaces;
    
  • URL: /components/raw/find-installer/SearchFieldPlaces.jsx
  • Filesystem Path: src/components/find-installer/SearchFieldPlaces.jsx
  • Size: 3.1 KB
  • Content:
    import React from 'react';
    import PropTypes from 'prop-types';
    import SearchFieldPlaces from './SearchFieldPlaces';
    
    const StartView = (props) => {
        return (
            <div className="find-installer-start">
                <div className="find-installer-start__panel">
                    <h1>{props.lang.startTitle}</h1>
                    <p className="preamble">
                        {props.lang.startDescription}
                    </p>
                    <SearchFieldPlaces
                        onSelect={props.onSelect}
                        geolocationLabel={props.lang.geolocation}
                        className="search-field--big-m h-no-margin-bottom"
                    />
                </div>
            </div>
        );
    };
    
    StartView.propTypes = {
        onSelect: PropTypes.func
    };
    
    StartView.defaultProps = {
        onSelect: () => {}
    };
    
    export default StartView;
    
  • URL: /components/raw/find-installer/StartView.jsx
  • Filesystem Path: src/components/find-installer/StartView.jsx
  • Size: 842 Bytes
  • Content:
    .find-installer {
        display: flex;
        width: 100%;
        align-items: center;
        justify-content: space-around;
    
        @include breakpoint($xl) {
            height: 70vh;
            min-height: size(60);
        }
    }
    
    // start view
    .find-installer-start {
        display: flex;
        align-items: center;
        justify-content: space-around;
        width: 100%;
        height: 100%;
        background: url("./img/start-background.png") center no-repeat;
        background-size: cover;
    }
    
    .find-installer-start__panel {
        width: 100%;
        max-width: size(116);
        background: $color-white;
        padding: size(4) size(2);
        margin: size(5) size(1);
        @include breakpoint($m) {
            padding: size(10) size(15);
            margin: size(6) size(2);
        }
    }
    
    // map view
    .find-installer-map {
        display: flex;
        flex: 1;
        height: 100%;
        flex-direction: column;
        flex-direction: column-reverse;
    
        @include breakpoint($xl) {
            flex-direction: row;
            flex-direction: initial;
        }
    }
    
    .find-installer-map__map {
        flex: 1 0 auto;
        height: 50vh;
    
        @include breakpoint($xl) {
            display: flex;
            flex-direction: column;
            height: 100%;
            width: size(52);
            padding: 0;
        }
    }
    
    // sidebar
    
    .find-installer-map__result {
        background: $color-gray-1;
        width: 100%;
        padding-top: size(3);
    
        @include breakpoint($xl) {
            display: flex;
            flex-direction: column;
            width: size(60);
            padding: size(3) size(2) 0;
            border-right: 1px solid $color-gray-2;
        }
    }
    
    .find-installer-map__search {
        // Offsets the search field's own margin, leaving 24px (20px on desktop) below it
        margin-bottom: size(-1);
    
        @include breakpoint($xl) {
            margin-bottom: size(-1.5);
        }
    }
    
    .find-installer-map__cards {
        flex: 1;
        overflow: auto;
    }
    
    // area filter
    .find-installer-filter {
        position: relative;
    
        .select-multiple {
            margin-bottom: size(3);
    
            @include breakpoint($xl) {
                margin-bottom: size(2.5);
            }
        }
    
        .select-multiple__wrapper {
            min-height: size(6);
            padding: size(1) size(6) 0 size(2);
            border-color: $color-gray-3;
        }
    
        .select-multiple__toggler {
            padding: 0 size(2);
            background-image: url('./img/icon-chevron-down.svg');
            background-size: size(2) size(2);
            font-family: inherit;
            font-size: size(2);
            line-height: size(3);
            font-weight: normal;
            color: $color-black;
        }
    
        .select-multiple__pill {
            margin: 0 size(1) size(1) 0;
            padding: size(1) size(3) size(1) size(1);
            border: 0;
            font-family: inherit;
            font-size: size(1.5);
            line-height: size(2);
            text-align: left;
    
            &:after {
                right: size(1);
                width: size(1);
                height: size(1);
                margin-top: size(-0.5);
                background-image: url('./img/icon-close.svg');
                background-size: contain;
            }
        }
    
        .select-multiple__options {
            margin: 0;
            padding: 0;
            list-style: none;
            max-height: size(40);
            border-color: $color-gray-3;
        }
    
        .select-multiple__option {
            width: 100%;
            padding: size(1) size(2);
            border: 0;
            background: none;
            font-family: inherit;
            font-size: size(1.75);
            line-height: size(2);
            text-align: left;
            color: $color-green-dark;
    
            &:hover,
            &:focus-visible {
                background: $color-gray-1;
                color: $color-green-dark;
            }
    
            @include color-theme {
                &,
                &:hover,
                &:focus-visible {
                    color: $arg-theme-color;
                }
            }
        }
    }
    
    .find-installer-filter__label {
        margin-bottom: size(1);
        font-size: size(2);
        line-height: size(3);
        font-weight: $font-weight-bold;
        color: $color-black;
    }
    
  • URL: /components/raw/find-installer/find-installer.scss
  • Filesystem Path: src/components/find-installer/find-installer.scss
  • Size: 3.9 KB
  • Content:
    .search-field-places__result {
        min-height: size(8);
        padding: size(2);
    }
    
    .search-field-places__icon-wrapper {
        width: size(4);
        height: size(4);
        margin-right: size(2);
        color: $color-green;
        flex-shrink: 0;
    
        @include color-theme {
            color: $arg-theme-color;
        }
    }
    
  • URL: /components/raw/find-installer/search-field-places.scss
  • Filesystem Path: src/components/find-installer/search-field-places.scss
  • Size: 300 Bytes

No notes defined.