@reykjavik/webtools 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/CookieHubConsent.d.ts +231 -0
- package/CookieHubConsent.js +127 -0
- package/README.md +452 -0
- package/esm/CookieHubConsent.d.ts +231 -0
- package/esm/CookieHubConsent.js +99 -0
- package/esm/http.d.ts +82 -0
- package/esm/http.js +125 -0
- package/esm/index.d.ts +6 -0
- package/esm/index.js +1 -0
- package/esm/next/SiteImprove.d.ts +81 -0
- package/esm/next/SiteImprove.js +100 -0
- package/esm/next/http.d.ts +56 -0
- package/esm/next/http.js +81 -0
- package/esm/package.json +1 -0
- package/http.d.ts +82 -0
- package/http.js +129 -0
- package/index.d.ts +6 -0
- package/index.js +2 -0
- package/next/SiteImprove.d.ts +81 -0
- package/next/SiteImprove.js +131 -0
- package/next/http.d.ts +56 -0
- package/next/http.js +103 -0
- package/package.json +47 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
2
|
+
// END: Mock typing of CookieHub's script API
|
|
3
|
+
// --------------------------------------------------------------------------
|
|
4
|
+
//
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
const idToken = '[ACCOUNT_ID]';
|
|
7
|
+
const scriptUrlTemplate = process.env.NODE_ENV === 'production'
|
|
8
|
+
? `https://cookiehub.net/c2/${idToken}.js`
|
|
9
|
+
: `https://cookiehub.net/dev/${idToken}.js`;
|
|
10
|
+
const CookieHubContext = createContext(undefined);
|
|
11
|
+
/**
|
|
12
|
+
* Used as the initial/default state when CookieHubProvider mounts and
|
|
13
|
+
* prepares to load the script
|
|
14
|
+
*/
|
|
15
|
+
const initialConsentState = {
|
|
16
|
+
consent: {
|
|
17
|
+
analytics: false,
|
|
18
|
+
preferences: false,
|
|
19
|
+
marketing: false,
|
|
20
|
+
uncategorized: false,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Moves the CookieHub `<div/>` to the bottom of the dom tree
|
|
25
|
+
* for better accessability in screen readers.
|
|
26
|
+
*/
|
|
27
|
+
const moveCookiehubScriptInDomTree = () => {
|
|
28
|
+
const cookieHubPromptElm = document.querySelector('.ch2');
|
|
29
|
+
if (cookieHubPromptElm) {
|
|
30
|
+
document.body.append(cookieHubPromptElm);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* This context provider component loads and initialises the CookieHub consent
|
|
35
|
+
* management script and sets up a React state object with the relevant user
|
|
36
|
+
* consent flags.
|
|
37
|
+
*
|
|
38
|
+
* @see https://github.com/reykjavikcity/webtools/tree/v0.1##cookiehubprovider-component
|
|
39
|
+
*/
|
|
40
|
+
export const CookieHubProvider = (props) => {
|
|
41
|
+
const [state, setState] = useState(initialConsentState);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const script = document.createElement('script');
|
|
44
|
+
script.async = true;
|
|
45
|
+
const opts = props.options || {};
|
|
46
|
+
script.src =
|
|
47
|
+
props.scriptUrl != null
|
|
48
|
+
? props.scriptUrl
|
|
49
|
+
: scriptUrlTemplate.replace(idToken, props.accountId);
|
|
50
|
+
script.onload = () => {
|
|
51
|
+
window.cookiehub.load(Object.assign(Object.assign({}, opts), { onInitialise(status) {
|
|
52
|
+
const analytics = this.hasConsented('analytics');
|
|
53
|
+
const preferences = this.hasConsented('preferences');
|
|
54
|
+
const marketing = this.hasConsented('marketing');
|
|
55
|
+
const uncategorized = this.hasConsented('uncategorized');
|
|
56
|
+
// only trigger re-render if the consent is different from the default (all false)
|
|
57
|
+
if (analytics || preferences || marketing || uncategorized) {
|
|
58
|
+
setState({
|
|
59
|
+
consent: {
|
|
60
|
+
analytics,
|
|
61
|
+
preferences,
|
|
62
|
+
marketing,
|
|
63
|
+
uncategorized,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
opts.onInitialise && opts.onInitialise.call(this, status);
|
|
68
|
+
},
|
|
69
|
+
onAllow(category) {
|
|
70
|
+
if (category === 'necessary') {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
setState((state) => (Object.assign(Object.assign({}, state), { consent: Object.assign(Object.assign({}, state.consent), { [category]: true }) })));
|
|
74
|
+
opts.onAllow && opts.onAllow.call(this, category);
|
|
75
|
+
},
|
|
76
|
+
onRevoke(category) {
|
|
77
|
+
if (category === 'necessary') {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
setState((state) => (Object.assign(Object.assign({}, state), { consent: Object.assign(Object.assign({}, state.consent), { [category]: false }) })));
|
|
81
|
+
opts.onAllow && opts.onAllow.call(this, category);
|
|
82
|
+
}, cookie: Object.assign({ secure: true }, opts.cookie) }));
|
|
83
|
+
moveCookiehubScriptInDomTree();
|
|
84
|
+
};
|
|
85
|
+
props.onError && (script.onerror = props.onError);
|
|
86
|
+
document.body.append(script);
|
|
87
|
+
},
|
|
88
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
89
|
+
[]);
|
|
90
|
+
return (React.createElement(CookieHubContext.Provider, { value: state }, props.children));
|
|
91
|
+
};
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
/**
|
|
94
|
+
* Returns up-to-date cookie consent flags. For use in React components or hook
|
|
95
|
+
* functions.
|
|
96
|
+
*
|
|
97
|
+
* @see https://github.com/reykjavikcity/webtools/tree/v0.1##usecookiehubconsent
|
|
98
|
+
*/
|
|
99
|
+
export const useCookieHubConsent = () => { var _a; return ((_a = useContext(CookieHubContext)) === null || _a === void 0 ? void 0 : _a.consent) || {}; };
|
package/esm/http.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ServerResponse } from 'http';
|
|
2
|
+
export declare const HTTP_200_OK = 200;
|
|
3
|
+
/**
|
|
4
|
+
* The request succeeded, and a new resource was created as a result.
|
|
5
|
+
* This is typically the response sent after POST requests,
|
|
6
|
+
* or some PUT requests.
|
|
7
|
+
**/
|
|
8
|
+
export declare const HTTP_201_Created = 201;
|
|
9
|
+
export declare const HTTP_202_Accepted = 202;
|
|
10
|
+
/**
|
|
11
|
+
* Only safe to use in response to GET and HEAD requests
|
|
12
|
+
*
|
|
13
|
+
* @deprecated Instead use `HTTP_308_PermanentRedirect`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const HTTP_301_MovedPermanently = 301;
|
|
16
|
+
/**
|
|
17
|
+
* Only safe to use in response to GET and HEAD requests
|
|
18
|
+
*
|
|
19
|
+
* @deprecated Instead use `HTTP_307_TemporaryRedirect`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const HTTP_302_Found = 302;
|
|
22
|
+
/** Use when POST or PUT successfully rediects to the created resource */
|
|
23
|
+
export declare const HTTP_303_SeeOther = 303;
|
|
24
|
+
/** Use in response GET and HEAD requests with `If-Modified-Since`/`If-None-Match` heaaders */
|
|
25
|
+
export declare const HTTP_304_NotModified = 304;
|
|
26
|
+
export declare const HTTP_307_TemporaryRedirect = 307;
|
|
27
|
+
export declare const HTTP_308_PermanentRedirect = 308;
|
|
28
|
+
/** The request is malformed (e.g. a URL param is of a wrong type) */
|
|
29
|
+
export declare const HTTP_400_BadRequest = 400;
|
|
30
|
+
/** User is not authenticated (i.e. not logged in) */
|
|
31
|
+
export declare const HTTP_401_Unauthorized = 401;
|
|
32
|
+
/** User is logged in but doesn't have the necessary privileges */
|
|
33
|
+
export declare const HTTP_403_Forbidden = 403;
|
|
34
|
+
/** The request looks OK but the resource does not exist */
|
|
35
|
+
export declare const HTTP_404_NotFound = 404;
|
|
36
|
+
/**
|
|
37
|
+
* The resource has been permanently deleted from server, with no forwarding
|
|
38
|
+
* address.
|
|
39
|
+
*/
|
|
40
|
+
export declare const HTTP_410_Gone = 410;
|
|
41
|
+
/** The server refuses the attempt to brew coffee with a teapot. */
|
|
42
|
+
export declare const HTTP_418_ImATeapot = 418;
|
|
43
|
+
export declare const HTTP_500_InternalServerError = 500;
|
|
44
|
+
export type HTTP_SUCCESS = typeof HTTP_200_OK | typeof HTTP_201_Created | typeof HTTP_202_Accepted;
|
|
45
|
+
export type HTTP_REDIRECTION = typeof HTTP_301_MovedPermanently | typeof HTTP_302_Found | typeof HTTP_303_SeeOther | typeof HTTP_304_NotModified | typeof HTTP_307_TemporaryRedirect | typeof HTTP_308_PermanentRedirect;
|
|
46
|
+
export type HTTP_NOTMODIFIED = typeof HTTP_304_NotModified;
|
|
47
|
+
export type HTTP_CLIENT_ERROR = typeof HTTP_400_BadRequest | typeof HTTP_401_Unauthorized | typeof HTTP_403_Forbidden | typeof HTTP_404_NotFound | typeof HTTP_410_Gone | typeof HTTP_418_ImATeapot;
|
|
48
|
+
export type HTTP_NOT_FOUND = typeof HTTP_400_BadRequest | typeof HTTP_404_NotFound | typeof HTTP_410_Gone;
|
|
49
|
+
export type HTTP_BANNED = typeof HTTP_401_Unauthorized | typeof HTTP_403_Forbidden;
|
|
50
|
+
export type HTTP_SERVER_ERROR = typeof HTTP_500_InternalServerError;
|
|
51
|
+
export type HTTP_ERROR = HTTP_CLIENT_ERROR | HTTP_SERVER_ERROR;
|
|
52
|
+
export type HTTP_STATUS = HTTP_SUCCESS | HTTP_REDIRECTION | HTTP_CLIENT_ERROR | HTTP_SERVER_ERROR;
|
|
53
|
+
type TimeUnit = 's' | 'm' | 'h' | 'd' | 'w';
|
|
54
|
+
type TTL = number | `${number}${TimeUnit}`;
|
|
55
|
+
type TTLKeywords = 'permanent' | 'unset' | 'no-cache';
|
|
56
|
+
type TTLObj = {
|
|
57
|
+
/** Sets the cache `max-age=` for the resource. */
|
|
58
|
+
maxAge: TTL | TTLKeywords;
|
|
59
|
+
/** Sets `stale-while-revalidate=` for the resource */
|
|
60
|
+
staleWhileRevalidate?: TTL;
|
|
61
|
+
staleIfError?: TTL;
|
|
62
|
+
/** Sets the response caching as "public", instead of the default "private" */
|
|
63
|
+
publ?: boolean;
|
|
64
|
+
/** Sets a 'must-revalidate' flag instead of the default 'immutable' */
|
|
65
|
+
stability?: 'revalidate' | 'immutable' | 'normal';
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Configures quick TTL-related settings for a HTTP request object
|
|
69
|
+
*
|
|
70
|
+
* @see https://github.com/reykjavikcity/webtools/tree/v0.1#type-ttlconfig
|
|
71
|
+
*/
|
|
72
|
+
export type TTLConfig = TTL | TTLKeywords | TTLObj;
|
|
73
|
+
/**
|
|
74
|
+
* Use this function to quickly set the `Cache-Control` header with a `max-age=`
|
|
75
|
+
* on a HTTP response
|
|
76
|
+
*
|
|
77
|
+
* @see https://github.com/reykjavikcity/webtools/tree/v0.1#getcssbundleurl
|
|
78
|
+
*/
|
|
79
|
+
export declare const cacheControl: (response: ServerResponse | {
|
|
80
|
+
res: ServerResponse;
|
|
81
|
+
}, ttlCfg: TTLConfig, eTag?: string | number) => void;
|
|
82
|
+
export {};
|
package/esm/http.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export const HTTP_200_OK = 200;
|
|
2
|
+
/**
|
|
3
|
+
* The request succeeded, and a new resource was created as a result.
|
|
4
|
+
* This is typically the response sent after POST requests,
|
|
5
|
+
* or some PUT requests.
|
|
6
|
+
**/
|
|
7
|
+
export const HTTP_201_Created = 201;
|
|
8
|
+
/*
|
|
9
|
+
* The request has been received but not yet acted upon.
|
|
10
|
+
* Uee in cases where another process or server handles the request.
|
|
11
|
+
*/
|
|
12
|
+
export const HTTP_202_Accepted = 202;
|
|
13
|
+
/**
|
|
14
|
+
* Only safe to use in response to GET and HEAD requests
|
|
15
|
+
*
|
|
16
|
+
* @deprecated Instead use `HTTP_308_PermanentRedirect`.
|
|
17
|
+
*/
|
|
18
|
+
export const HTTP_301_MovedPermanently = 301;
|
|
19
|
+
/**
|
|
20
|
+
* Only safe to use in response to GET and HEAD requests
|
|
21
|
+
*
|
|
22
|
+
* @deprecated Instead use `HTTP_307_TemporaryRedirect`.
|
|
23
|
+
*/
|
|
24
|
+
export const HTTP_302_Found = 302;
|
|
25
|
+
/** Use when POST or PUT successfully rediects to the created resource */
|
|
26
|
+
export const HTTP_303_SeeOther = 303;
|
|
27
|
+
/** Use in response GET and HEAD requests with `If-Modified-Since`/`If-None-Match` heaaders */
|
|
28
|
+
export const HTTP_304_NotModified = 304;
|
|
29
|
+
export const HTTP_307_TemporaryRedirect = 307;
|
|
30
|
+
export const HTTP_308_PermanentRedirect = 308;
|
|
31
|
+
/** The request is malformed (e.g. a URL param is of a wrong type) */
|
|
32
|
+
export const HTTP_400_BadRequest = 400;
|
|
33
|
+
/** User is not authenticated (i.e. not logged in) */
|
|
34
|
+
export const HTTP_401_Unauthorized = 401;
|
|
35
|
+
/** User is logged in but doesn't have the necessary privileges */
|
|
36
|
+
export const HTTP_403_Forbidden = 403;
|
|
37
|
+
/** The request looks OK but the resource does not exist */
|
|
38
|
+
export const HTTP_404_NotFound = 404;
|
|
39
|
+
/**
|
|
40
|
+
* The resource has been permanently deleted from server, with no forwarding
|
|
41
|
+
* address.
|
|
42
|
+
*/
|
|
43
|
+
export const HTTP_410_Gone = 410;
|
|
44
|
+
/** The server refuses the attempt to brew coffee with a teapot. */
|
|
45
|
+
export const HTTP_418_ImATeapot = 418;
|
|
46
|
+
export const HTTP_500_InternalServerError = 500;
|
|
47
|
+
const unitToSeconds = {
|
|
48
|
+
s: 1,
|
|
49
|
+
m: 60,
|
|
50
|
+
h: 3600,
|
|
51
|
+
d: 24 * 3600,
|
|
52
|
+
w: 7 * 24 * 3600,
|
|
53
|
+
};
|
|
54
|
+
const toSec = (ttl) => {
|
|
55
|
+
if (ttl == null) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (typeof ttl === 'string') {
|
|
59
|
+
const value = parseFloat(ttl);
|
|
60
|
+
const factor = unitToSeconds[ttl.slice(-1)] || 1;
|
|
61
|
+
ttl = value * factor;
|
|
62
|
+
}
|
|
63
|
+
return !isNaN(ttl) ? ttl : undefined;
|
|
64
|
+
};
|
|
65
|
+
const stabilities = {
|
|
66
|
+
revalidate: ', must-revalidate',
|
|
67
|
+
immutable: ', immutable',
|
|
68
|
+
normal: '',
|
|
69
|
+
};
|
|
70
|
+
const setCC = (response, cc) => {
|
|
71
|
+
const devModeHeader = 'X-Cache-Control';
|
|
72
|
+
// Also set `X-Cache-Control` in dev mode, because some frameworks
|
|
73
|
+
// **cough** **nextjs** **cough** forcefully override the `Cache-Control`
|
|
74
|
+
// header when the server is in dev mode.
|
|
75
|
+
if (!cc) {
|
|
76
|
+
response.removeHeader('Cache-Control');
|
|
77
|
+
process.env.NODE_ENV !== 'production' && response.removeHeader(devModeHeader);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
response.setHeader('Cache-Control', cc);
|
|
81
|
+
process.env.NODE_ENV !== 'production' && response.setHeader(devModeHeader, cc);
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Use this function to quickly set the `Cache-Control` header with a `max-age=`
|
|
85
|
+
* on a HTTP response
|
|
86
|
+
*
|
|
87
|
+
* @see https://github.com/reykjavikcity/webtools/tree/v0.1#getcssbundleurl
|
|
88
|
+
*/
|
|
89
|
+
// eslint-disable-next-line complexity
|
|
90
|
+
export const cacheControl = (response, ttlCfg, eTag) => {
|
|
91
|
+
response = 'res' in response ? response.res : response;
|
|
92
|
+
const opts = typeof ttlCfg === 'number' || typeof ttlCfg === 'string'
|
|
93
|
+
? { maxAge: ttlCfg }
|
|
94
|
+
: ttlCfg;
|
|
95
|
+
let maxAge = opts.maxAge;
|
|
96
|
+
if (typeof maxAge === 'string') {
|
|
97
|
+
if (maxAge === 'permanent') {
|
|
98
|
+
maxAge = 365 * unitToSeconds.d;
|
|
99
|
+
}
|
|
100
|
+
else if (maxAge === 'no-cache') {
|
|
101
|
+
maxAge = 0;
|
|
102
|
+
}
|
|
103
|
+
else if (maxAge === 'unset') {
|
|
104
|
+
maxAge = undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
maxAge = toSec(maxAge);
|
|
108
|
+
if (maxAge == null) {
|
|
109
|
+
response.removeHeader('Cache-Control');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const sWR_ttl = toSec(opts.staleWhileRevalidate);
|
|
113
|
+
const sWR = sWR_ttl != null ? `, stale-while-revalidate=${sWR_ttl}` : '';
|
|
114
|
+
const sIE_ttl = toSec(opts.staleIfError);
|
|
115
|
+
const sIE = sIE_ttl != null ? `, stale-if-error=${sIE_ttl}` : '';
|
|
116
|
+
maxAge = Math.round(maxAge);
|
|
117
|
+
if (maxAge <= 0) {
|
|
118
|
+
setCC(response, 'no-cache');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const scope = opts.publ ? 'public' : 'private';
|
|
122
|
+
const stability = (opts.stability && stabilities[opts.stability]) || stabilities.immutable;
|
|
123
|
+
eTag != null && response.setHeader('ETag', eTag);
|
|
124
|
+
setCC(response, `${scope}, max-age=${maxAge + sWR + sIE + stability}`);
|
|
125
|
+
};
|
package/esm/index.d.ts
ADDED
package/esm/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { EitherObj } from '@reykjavik/hanna-utils';
|
|
3
|
+
declare global {
|
|
4
|
+
interface Window {
|
|
5
|
+
_sz?: Array<SiteImproveEvent> & {
|
|
6
|
+
/**
|
|
7
|
+
* Set if posting a tracking event is attempted before SiteImprove has
|
|
8
|
+
* been initialized, and the window._sz array had to be initialized
|
|
9
|
+
* just-in-time.
|
|
10
|
+
*/
|
|
11
|
+
_jit_defined_?: true;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
type SiteImproveEvent = SiteImprovePageView | SiteImproveCustomEvent;
|
|
16
|
+
type SiteImprovePageView = [
|
|
17
|
+
type: 'trackdynamic',
|
|
18
|
+
data: {
|
|
19
|
+
/** New page URL */
|
|
20
|
+
url: string;
|
|
21
|
+
/** The previous (referer) URL */
|
|
22
|
+
ref: string;
|
|
23
|
+
/** New page title */
|
|
24
|
+
title?: string;
|
|
25
|
+
}
|
|
26
|
+
];
|
|
27
|
+
type SiteImproveCustomEvent = [
|
|
28
|
+
type: 'event',
|
|
29
|
+
category: string,
|
|
30
|
+
action: string,
|
|
31
|
+
label?: string
|
|
32
|
+
];
|
|
33
|
+
export type SiteImproveProps = EitherObj<{
|
|
34
|
+
/**
|
|
35
|
+
* Your SiteImprove account ID.
|
|
36
|
+
*
|
|
37
|
+
* It's a random-looking numerical string, and it can usually be
|
|
38
|
+
* extracted from the script embed URL like this:
|
|
39
|
+
* `"https://siteimproveanalytics.com/js/siteanalyze_[ACCOUNT_ID].js"`
|
|
40
|
+
*/
|
|
41
|
+
accountId: string;
|
|
42
|
+
}, {
|
|
43
|
+
/**
|
|
44
|
+
* The full SiteImprove analytics script URL.
|
|
45
|
+
*
|
|
46
|
+
* Something like `"https://siteimproveanalytics.com/js/siteanalyze_[ACCOUNT_ID].js"`
|
|
47
|
+
*/
|
|
48
|
+
scriptUrl: string;
|
|
49
|
+
}> & {
|
|
50
|
+
/**
|
|
51
|
+
* Manual GDPR 'analytics' consent flag.
|
|
52
|
+
*
|
|
53
|
+
* A value of `false` prevents the analytics script being loaded.
|
|
54
|
+
*
|
|
55
|
+
* Any other value causes the component to defer to the `CookieHubProvider`
|
|
56
|
+
* component, and only applies if the cookiehub flag is undefined.
|
|
57
|
+
*/
|
|
58
|
+
hasConstented?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Custom callback for when the SiteImprove script has loaded.
|
|
61
|
+
*/
|
|
62
|
+
onLoad?: (e: unknown) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Error callback for if the SiteImprove script fails to load.
|
|
65
|
+
*/
|
|
66
|
+
onError?: (e: unknown) => void;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* A component for loading a SiteImprove analytics script and set up page-view
|
|
70
|
+
* tracking across Next.js routes.
|
|
71
|
+
*
|
|
72
|
+
* @see https://github.com/reykjavikcity/webtools/tree/v0.1##siteimprove-component
|
|
73
|
+
*/
|
|
74
|
+
export declare const SiteImprove: (props: SiteImproveProps) => JSX.Element | null;
|
|
75
|
+
/**
|
|
76
|
+
* A small helper for tracking custom UI events and reporting them to SiteImrove.
|
|
77
|
+
*
|
|
78
|
+
* @see https://github.com/reykjavikcity/webtools/tree/v0.1##pingsiteimprove-helper
|
|
79
|
+
*/
|
|
80
|
+
export declare const pingSiteImprove: (category: string, action: string, label?: string) => void;
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { Router } from 'next/router';
|
|
3
|
+
import Script from 'next/script';
|
|
4
|
+
import { useCookieHubConsent } from '../CookieHubConsent';
|
|
5
|
+
// END: Mock typing of SiteImprove's event tracking API
|
|
6
|
+
// --------------------------------------------------------------------------
|
|
7
|
+
//
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
const _emitEvent = typeof window === 'undefined'
|
|
10
|
+
? () => undefined
|
|
11
|
+
: (event) => {
|
|
12
|
+
let _sz = window._sz;
|
|
13
|
+
if (!_sz) {
|
|
14
|
+
_sz = window._sz = [];
|
|
15
|
+
_sz._jit_defined_ = true;
|
|
16
|
+
}
|
|
17
|
+
if (process.env.NODE_ENV === 'development') {
|
|
18
|
+
console.info('SiteImprove:', event);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
_sz.push(event);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/*
|
|
25
|
+
SiteImprove's "trackdynamic" (page view) event requires both the new URL
|
|
26
|
+
and the old (referer) URL.
|
|
27
|
+
Next router's `routeChangeComplete` (which is the correct point in time
|
|
28
|
+
to send the tracking event) does not provide access to the previous URL,
|
|
29
|
+
so we need to capture it during `routeChangeStart`.
|
|
30
|
+
We feel it's safe to assume that every `routeChangeComplete` event
|
|
31
|
+
always fires directly after its `routeChangeStart` counterpart,
|
|
32
|
+
and it is thus safe to simply store the old URL in a local variable.
|
|
33
|
+
This may look dodgy, but should prove reasonably safe in practice.
|
|
34
|
+
*/
|
|
35
|
+
let refUrl = '';
|
|
36
|
+
const captureRefUrl = () => {
|
|
37
|
+
refUrl = document.location.pathname + document.location.search;
|
|
38
|
+
};
|
|
39
|
+
const sendRoutingEvent = (url) => _emitEvent([
|
|
40
|
+
'trackdynamic',
|
|
41
|
+
{
|
|
42
|
+
url,
|
|
43
|
+
ref: refUrl,
|
|
44
|
+
title: document.title,
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
const idToken = '[ACCOUNT_ID]';
|
|
49
|
+
const scriptUrlTemplate = `https://siteimproveanalytics.com/js/siteanalyze_${idToken}.js`;
|
|
50
|
+
/**
|
|
51
|
+
* A component for loading a SiteImprove analytics script and set up page-view
|
|
52
|
+
* tracking across Next.js routes.
|
|
53
|
+
*
|
|
54
|
+
* @see https://github.com/reykjavikcity/webtools/tree/v0.1##siteimprove-component
|
|
55
|
+
*/
|
|
56
|
+
export const SiteImprove = (props) => {
|
|
57
|
+
const { analytics } = useCookieHubConsent();
|
|
58
|
+
const consented = (analytics && props.hasConstented !== false) ||
|
|
59
|
+
(analytics === undefined && props.hasConstented);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (consented) {
|
|
62
|
+
const routerEvents = Router.events;
|
|
63
|
+
routerEvents.on('routeChangeStart', captureRefUrl);
|
|
64
|
+
routerEvents.on('routeChangeComplete', sendRoutingEvent);
|
|
65
|
+
return () => {
|
|
66
|
+
routerEvents.off('routeChangeStart', captureRefUrl);
|
|
67
|
+
routerEvents.off('routeChangeComplete', sendRoutingEvent);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}, [consented]);
|
|
71
|
+
if (!consented) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
75
|
+
console.info('Mock loading SiteImprove in development mode.');
|
|
76
|
+
if (!window._sz) {
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
window._sz = window._sz || [];
|
|
79
|
+
}, 300);
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const scriptUrl = props.scriptUrl != null
|
|
84
|
+
? props.scriptUrl
|
|
85
|
+
: scriptUrlTemplate.replace(idToken, props.accountId);
|
|
86
|
+
return (React.createElement(Script, { type: "text/javascript", strategy: "afterInteractive", src: scriptUrl, onLoad: props.onLoad, onError: props.onError }));
|
|
87
|
+
};
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
/**
|
|
90
|
+
* A small helper for tracking custom UI events and reporting them to SiteImrove.
|
|
91
|
+
*
|
|
92
|
+
* @see https://github.com/reykjavikcity/webtools/tree/v0.1##pingsiteimprove-helper
|
|
93
|
+
*/
|
|
94
|
+
export const pingSiteImprove = (category, action, label) => {
|
|
95
|
+
if (process.env.NODE_ENV === 'development' &&
|
|
96
|
+
(!window._sz || window._sz._jit_defined_)) {
|
|
97
|
+
console.warn('`pingSiteImprove` Was called before SiteImprove script was loaded.');
|
|
98
|
+
}
|
|
99
|
+
_emitEvent(['event', category, action, label]);
|
|
100
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import React, { FunctionComponent } from 'react';
|
|
3
|
+
import { Cleanup } from '@reykjavik/hanna-utils';
|
|
4
|
+
import { ServerResponse } from 'http';
|
|
5
|
+
import type { AppType } from 'next/app';
|
|
6
|
+
import type { HTTP_ERROR, TTLConfig } from '../http';
|
|
7
|
+
export * from '../http';
|
|
8
|
+
type NextContextLike = {
|
|
9
|
+
res: ServerResponse;
|
|
10
|
+
};
|
|
11
|
+
export type ErrorProps = {
|
|
12
|
+
statusCode: HTTP_ERROR;
|
|
13
|
+
/** If a HTTP_ERROR code is passed, a default error message is displayed */
|
|
14
|
+
message?: string;
|
|
15
|
+
};
|
|
16
|
+
type ErrorizedPageProps<EP extends ErrorProps = ErrorProps> = {
|
|
17
|
+
__error: ErrorProps;
|
|
18
|
+
} & Omit<EP, keyof ErrorProps>;
|
|
19
|
+
type ShowErrorPageFn<EP extends ErrorProps = ErrorProps> = (response: ServerResponse | NextContextLike, error: (ErrorProps extends EP ? HTTP_ERROR : never) | EP,
|
|
20
|
+
/** Defaults to `"2s"`. Gets forwarded on to the `cacheControl` helper from `@reykjavik/webtools/http` */
|
|
21
|
+
ttl?: TTLConfig) => {
|
|
22
|
+
props: ErrorizedPageProps<EP>;
|
|
23
|
+
};
|
|
24
|
+
export type InferErrorPageProps<SEP extends ShowErrorPageFn<any>> = Cleanup<ReturnType<SEP>['props']>;
|
|
25
|
+
/**
|
|
26
|
+
* Hhigher-order component (HOC) factory for Next.js App compnents to handle
|
|
27
|
+
* cases when `getServerSideProps` returns an `__error` prop with `statusCode`
|
|
28
|
+
* and optional friendly `message`.
|
|
29
|
+
*
|
|
30
|
+
* @see https://github.com/reykjavikcity/webtools/tree/v0.1#makeerrorizeapphoc
|
|
31
|
+
*/
|
|
32
|
+
export declare const makeErrorizeAppHOC: <EP extends Partial<ErrorProps>>(ErrorPage: React.FunctionComponent<EP>) => {
|
|
33
|
+
<P extends {
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
__error?: undefined;
|
|
36
|
+
}>(App: AppType<P>): React.FunctionComponent<import("next/dist/shared/lib/utils").AppPropsType<any, P | ({
|
|
37
|
+
__error: ErrorProps;
|
|
38
|
+
} & Omit<ErrorProps & EP, keyof ErrorProps>)>> & {
|
|
39
|
+
getInitialProps?(context: import("next/dist/shared/lib/utils").AppContextType<import("next/router").NextRouter>): P | ({
|
|
40
|
+
__error: ErrorProps;
|
|
41
|
+
} & Omit<ErrorProps & EP, keyof ErrorProps>) | Promise<P | ({
|
|
42
|
+
__error: ErrorProps;
|
|
43
|
+
} & Omit<ErrorProps & EP, keyof ErrorProps>)>;
|
|
44
|
+
};
|
|
45
|
+
showErrorPage: ShowErrorPageFn<ErrorProps & EP>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Use this method to inside a `getServerSideProps` method (or API route)
|
|
49
|
+
* to return an `HTTP_304_NotModified` response with an empty props object,
|
|
50
|
+
* in a way that doesn't make TypeScript at you.
|
|
51
|
+
*
|
|
52
|
+
* @see https://github.com/reykjavikcity/webtools/tree/v0.1#notmodified304-helper
|
|
53
|
+
*/
|
|
54
|
+
export declare const notModified304: (response: ServerResponse | NextContextLike) => {
|
|
55
|
+
readonly props: any;
|
|
56
|
+
};
|
package/esm/next/http.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { cacheControl, HTTP_304_NotModified } from '../http';
|
|
14
|
+
/*
|
|
15
|
+
Re-export all of the base [http module](#reykjavikwebtoolshttp)'s exports,
|
|
16
|
+
purely for convenience.
|
|
17
|
+
*/
|
|
18
|
+
export * from '../http';
|
|
19
|
+
/**
|
|
20
|
+
* Use this method inside a `getServerSideProps` method (or API route)
|
|
21
|
+
* to return an error page with proper HTTP status code and all the shit.
|
|
22
|
+
*
|
|
23
|
+
* @see https://github.com/reykjavikcity/webtools/tree/v0.1#showerrorpage-helper
|
|
24
|
+
*/
|
|
25
|
+
const showErrorPage = (response, error, ttl = '2s') => {
|
|
26
|
+
error =
|
|
27
|
+
typeof error === 'number'
|
|
28
|
+
? { statusCode: error }
|
|
29
|
+
: error;
|
|
30
|
+
const { statusCode, message } = error, otherProps = __rest(error, ["statusCode", "message"]);
|
|
31
|
+
response = 'res' in response ? response.res : response;
|
|
32
|
+
response.statusCode = error.statusCode;
|
|
33
|
+
cacheControl(response, ttl);
|
|
34
|
+
return {
|
|
35
|
+
props: Object.assign(Object.assign({}, otherProps), { __error: { statusCode, message } }),
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
// ===========================================================================
|
|
39
|
+
/**
|
|
40
|
+
* Hhigher-order component (HOC) factory for Next.js App compnents to handle
|
|
41
|
+
* cases when `getServerSideProps` returns an `__error` prop with `statusCode`
|
|
42
|
+
* and optional friendly `message`.
|
|
43
|
+
*
|
|
44
|
+
* @see https://github.com/reykjavikcity/webtools/tree/v0.1#makeerrorizeapphoc
|
|
45
|
+
*/
|
|
46
|
+
export const makeErrorizeAppHOC = (ErrorPage) => {
|
|
47
|
+
// the HOC
|
|
48
|
+
const withErrorHandling = (App) => {
|
|
49
|
+
const ErrorizedApp = (appProps) => {
|
|
50
|
+
const { pageProps } = appProps;
|
|
51
|
+
if (pageProps.__error) {
|
|
52
|
+
const { __error } = pageProps, otherProps = __rest(pageProps, ["__error"]);
|
|
53
|
+
return (React.createElement(App, Object.assign({}, appProps, { Component: ErrorPage, pageProps: Object.assign(Object.assign({}, otherProps), __error) })));
|
|
54
|
+
}
|
|
55
|
+
return React.createElement(App, Object.assign({}, appProps));
|
|
56
|
+
};
|
|
57
|
+
ErrorizedApp.getInitialProps = App.getInitialProps;
|
|
58
|
+
ErrorizedApp.displayName = 'Errorized' + (App.displayName || App.name || 'App');
|
|
59
|
+
return ErrorizedApp;
|
|
60
|
+
};
|
|
61
|
+
withErrorHandling.showErrorPage = showErrorPage;
|
|
62
|
+
return withErrorHandling;
|
|
63
|
+
};
|
|
64
|
+
// ===========================================================================
|
|
65
|
+
/**
|
|
66
|
+
* Use this method to inside a `getServerSideProps` method (or API route)
|
|
67
|
+
* to return an `HTTP_304_NotModified` response with an empty props object,
|
|
68
|
+
* in a way that doesn't make TypeScript at you.
|
|
69
|
+
*
|
|
70
|
+
* @see https://github.com/reykjavikcity/webtools/tree/v0.1#notmodified304-helper
|
|
71
|
+
*/
|
|
72
|
+
export const notModified304 = (response) => {
|
|
73
|
+
response = 'res' in response ? response.res : response;
|
|
74
|
+
response.statusCode = HTTP_304_NotModified;
|
|
75
|
+
return {
|
|
76
|
+
props:
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
{},
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
// ---------------------------------------------------------------------------
|
package/esm/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|