Skip to content

Commit

Permalink
feat: postcode resolving (wip)
Browse files Browse the repository at this point in the history
  • Loading branch information
DanWebb committed Feb 12, 2024
1 parent 8353ebb commit 3b348cc
Show file tree
Hide file tree
Showing 20 changed files with 187 additions and 89 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"sonarlint.connectedMode.project": {
"connectionId": "Etch",
"projectKey": "etchteam_recycling-locator-widget"
}
},
"typescript.tsdk": "node_modules/typescript/lib"
}
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"dependencies": {
"@etchteam/diamond-ui": "^1.18.0",
"@here/maps-api-for-javascript": "^1.50.0",
"@preact/signals": "^1.2.2",
"i18next": "^23.7.18",
"i18next-http-backend": "^2.4.2",
Expand Down
33 changes: 18 additions & 15 deletions public/translations/cy.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
{
"start": {
"title": "Dod o hyd i leoedd i ailgylchu."
},
"about": {
"title": "About this service",
"intro": "Powered by RecycleNow.com, this tool can be used to search and find recycling locations throughout the United Kingdom.",
"becomeAPartner": {
"title": "Add this tool to your website",
"description": "It’s been created as an embeddable widget that can be added to any website to help visitors discover more recycling options. For example, a beverage manufacturer could use the tool to show places to recycle drinks cans.",
"cta": "Become a partner",
"url": "https://wrap.org.uk/taking-action/citizen-behaviour-change/recycle-now/recycling-locator-tool"
"title": "Dod o hyd i leoedd i ailgylchu.",
"location": {
"title": "What do you need to recycle?"
},
"feedback": {
"title": "Improving this service",
"description": "We’re always looking to improve this tool and would be interested to hear about your experience whilst using it, good or bad.",
"cta": "Feedback on this tool",
"url": "https://wrapcymru.org.uk/contact-wrap-cymru"
"about": {
"title": "About this service",
"intro": "Powered by RecycleNow.com, this tool can be used to search and find recycling locations throughout the United Kingdom.",
"becomeAPartner": {
"title": "Add this tool to your website",
"description": "It’s been created as an embeddable widget that can be added to any website to help visitors discover more recycling options. For example, a beverage manufacturer could use the tool to show places to recycle drinks cans.",
"cta": "Become a partner",
"url": "https://wrap.org.uk/taking-action/citizen-behaviour-change/recycle-now/recycling-locator-tool"
},
"feedback": {
"title": "Improving this service",
"description": "We’re always looking to improve this tool and would be interested to hear about your experience whilst using it, good or bad.",
"cta": "Feedback on this tool",
"url": "https://wrapcymru.org.uk/contact-wrap-cymru"
}
}
}
}
33 changes: 18 additions & 15 deletions public/translations/en.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
{
"start": {
"title": "Find places to recycle."
},
"about": {
"title": "About this service",
"intro": "Powered by RecycleNow.com, this tool can be used to search and find recycling locations throughout the United Kingdom.",
"becomeAPartner": {
"title": "Add this tool to your website",
"description": "It’s been created as an embeddable widget that can be added to any website to help visitors discover more recycling options. For example, a beverage manufacturer could use the tool to show places to recycle drinks cans.",
"cta": "Become a partner",
"url": "https://wrap.org.uk/taking-action/citizen-behaviour-change/recycle-now/recycling-locator-tool"
"title": "Find places to recycle.",
"location": {
"title": "What do you need to recycle?"
},
"feedback": {
"title": "Improving this service",
"description": "We’re always looking to improve this tool and would be interested to hear about your experience whilst using it, good or bad.",
"cta": "Feedback on this tool",
"url": "https://wrap.org.uk/contact-us"
"about": {
"title": "About this service",
"intro": "Powered by RecycleNow.com, this tool can be used to search and find recycling locations throughout the United Kingdom.",
"becomeAPartner": {
"title": "Add this tool to your website",
"description": "It’s been created as an embeddable widget that can be added to any website to help visitors discover more recycling options. For example, a beverage manufacturer could use the tool to show places to recycle drinks cans.",
"cta": "Become a partner",
"url": "https://wrap.org.uk/taking-action/citizen-behaviour-change/recycle-now/recycling-locator-tool"
},
"feedback": {
"title": "Improving this service",
"description": "We’re always looking to improve this tool and would be interested to hear about your experience whilst using it, good or bad.",
"cta": "Feedback on this tool",
"url": "https://wrap.org.uk/contact-us"
}
}
}
}
2 changes: 1 addition & 1 deletion src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CustomElement } from '../../types/custom-element';

declare module 'preact' {
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'locator-header': CustomElement;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default function Icon({ icon, label }: IconAttributes) {

register(Icon, 'locator-icon', ['icon']);

declare module 'preact' {
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'locator-icon': CustomElement<IconAttributes>;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function Layout({

register(Layout, 'locator-layout', [], { shadow: true });

declare module 'preact' {
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'locator-layout': CustomElement;
Expand Down
4 changes: 3 additions & 1 deletion src/components/LocationInput/LocationInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Signal, signal } from '@preact/signals';
import { Component } from 'preact';
import register from 'preact-custom-element';
import '@etchteam/diamond-ui/control/Input/Input';

import config from '../../config';
import { CustomElement } from '../../types/custom-element';
Expand Down Expand Up @@ -66,6 +67,7 @@ export default class LocationInput extends Component<LocationInputProps> {
<locator-icon icon="pin" color="primary" />
<input
type="text"
name="location"
id={inputId}
list={listId}
onInput={this.handleInput}
Expand All @@ -85,7 +87,7 @@ export default class LocationInput extends Component<LocationInputProps> {

register(LocationInput, 'locator-location-input', ['inputId']);

declare module 'preact' {
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'locator-location-input': CustomElement<LocationInputProps>;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Logo/Logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default function Logo() {

register(Logo, 'locator-logo');

declare module 'preact' {
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'locator-logo': CustomElement;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Tip/Tip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export interface TipAttributes {
'text-align'?: 'center';
}

declare module 'preact' {
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'locator-tip': CustomElement<TipAttributes>;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Wrap/Wrap.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CustomElement } from '../../types/custom-element';

declare module 'preact' {
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'locator-wrap': CustomElement;
Expand Down
87 changes: 87 additions & 0 deletions src/lib/PostcodeResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { service } from '@here/maps-api-for-javascript';

import config from '../config';

interface HereMapsGeocodeResponse {
items: {
position: {
lat: number;
lng: number;
};
}[];
}

export default class PostCodeResolver {
private static ERROR_POSTCODE_NOT_FOUND = 'Postcode not found';
private static ERROR_SEARCH_FAILED = 'Search failed';

private static async getLatLng(
location: string,
): Promise<{ lat: number; lng: number }> {
const { default: H } = await import(
// @ts-expect-error TS can't find the maps types
'@here/maps-api-for-javascript/bin/mapsjs.bundle'
);
const apikey = config.mapsPlacesKey;
const platform = new H.service.Platform({ apikey });
const service = platform.getSearchService() as service.GeocodingService;
const result: HereMapsGeocodeResponse = await new Promise(
(resolve, reject) => {
service.geocode({ q: location }, resolve, reject);
},
);

if (result?.items?.length === 0) {
throw new Error(PostCodeResolver.ERROR_SEARCH_FAILED);
}

return result.items[0].position;
}

private static extractPostcodeFromString(
locationOrPostcode: string,
): string | null {
const matches = locationOrPostcode.match(
/(GIR ?0AA|[A-PR-UWYZ]([0-9]{1,2}|([A-HK-Y][0-9]([0-9ABEHMNPRV-Y])?)|[0-9][A-HJKPS-UW]) ?[0-9][ABD-HJLNP-UW-Z]{2})/i,
);

if (!matches) {
return null;
}

return matches[0].toUpperCase().replace(/ /g, '');
}

static async fromLatLng(lat: number, lng: number): Promise<string> {
// var url = config.widgetRoutePrefix + '/postcode/' +
// encodeURIComponent(latitude) + ',' +
// encodeURIComponent(longitude) +
// (window.location.hostname === 'localhost' ? '' : '?callback=?');
// $.getJSON({
// url: url
// }).done(function(data) {
// if (data.error && data.error == 'Not Found') {
// deferred.reject(resolver.ERROR_POSTCODE_NOT_FOUND);
// } else if (data.error) {
// deferred.reject(resolver.ERROR_SEARCH_FAILED);
// } else {
// deferred.resolve(data.postcode);
// }
// }).fail(function() {
// deferred.reject(resolver.ERROR_SEARCH_FAILED);
// });
return `${lat} ${lng}`;
}

static async fromString(locationOrPostcode: string): Promise<string> {
const postcode =
PostCodeResolver.extractPostcodeFromString(locationOrPostcode);

if (postcode) {
return postcode;
}

const { lat, lng } = await PostCodeResolver.getLatLng(locationOrPostcode);
return PostCodeResolver.fromLatLng(lat, lng);
}
}
8 changes: 5 additions & 3 deletions src/pages/Entrypoint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import { Locale } from '../types/locale';
import NotFound from './404';
import AboutPage from './Start/About';
import StartLayout from './Start/Layout';
import StartPage from './Start/Start';
import StartLocationPage from './Start/Location';
import StartPage, { startPageAction } from './Start/Start';

const router = createMemoryRouter(
createRoutesFromElements(
<Route element={<StartLayout />} errorElement={NotFound}>
<Route path="/" element={<StartPage />} />
<Route element={<StartLayout />} errorElement={<NotFound />}>
<Route path="/" element={<StartPage />} action={startPageAction} />
<Route path="/about" element={<AboutPage />} />
<Route path="/:postcode" element={<StartLocationPage />} />
</Route>,
),
);
Expand Down
23 changes: 13 additions & 10 deletions src/pages/Start/About.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
import { useTranslation } from 'react-i18next';
import '@etchteam/diamond-ui/control/Button/Button';

export default function AboutPage() {
const { t } = useTranslation();

return (
<>
<h2>{t('about.title')}</h2>
<p>{t('about.intro')}</p>
<h2>{t('start.about.title')}</h2>
<p>{t('start.about.intro')}</p>
<h3 className="diamond-spacing-top-md">
{t('about.becomeAPartner.title')}
{t('start.about.becomeAPartner.title')}
</h3>
<p>{t('about.becomeAPartner.description')}</p>
<p>{t('start.about.becomeAPartner.description')}</p>
<diamond-button width="full-width">
<a
href={t('about.becomeAPartner.url')}
href={t('start.about.becomeAPartner.url')}
target="_blank"
rel="noopener noreferrer"
>
{t('about.becomeAPartner.cta')}
{t('start.about.becomeAPartner.cta')}
</a>
</diamond-button>
<h3 className="diamond-spacing-top-md">{t('about.feedback.title')}</h3>
<p>{t('about.feedback.description')}</p>
<h3 className="diamond-spacing-top-md">
{t('start.about.feedback.title')}
</h3>
<p>{t('start.about.feedback.description')}</p>
<diamond-button width="full-width">
<a
href={t('about.feedback.url')}
href={t('start.about.feedback.url')}
target="_blank"
rel="noopener noreferrer"
>
{t('about.feedback.cta')}
{t('start.about.feedback.cta')}
</a>
</diamond-button>
</>
Expand Down
6 changes: 3 additions & 3 deletions src/pages/Start/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Outlet, Link, useHref } from 'react-router-dom';
import { Outlet, Link, useLocation } from 'react-router-dom';

import '../../components/Layout/Layout';
import '../../components/Logo/Logo';
Expand All @@ -21,13 +21,13 @@ function InfoButton({ open }: { readonly open: boolean }) {
}

export default function StartLayout() {
const path = useHref();
const location = useLocation();

return (
<locator-layout>
<locator-header slot="header">
<locator-logo slot="header"></locator-logo>
<InfoButton open={path === '/about'} />
<InfoButton open={location.pathname === '/about'} />
</locator-header>
<locator-wrap slot="main">
<diamond-section padding="lg">
Expand Down
7 changes: 7 additions & 0 deletions src/pages/Start/Location.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useTranslation } from 'react-i18next';

export default function StartLocationPage() {
const { t } = useTranslation();

return <h2>{t('start.location.title')}</h2>;
}
Loading

0 comments on commit 3b348cc

Please sign in to comment.