@justdanielndev/status-page 1.17.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.
Files changed (52) hide show
  1. package/.prettierrc.cjs +1 -0
  2. package/.upptimerc.yml +42 -0
  3. package/CHANGELOG.md +791 -0
  4. package/LICENSE +21 -0
  5. package/README.md +46 -0
  6. package/cypress/fixtures/example.json +5 -0
  7. package/cypress/integration/incident.spec.js +9 -0
  8. package/cypress/integration/live-status.spec.js +12 -0
  9. package/cypress/plugins/index.js +17 -0
  10. package/cypress/support/commands.js +25 -0
  11. package/cypress/support/index.js +20 -0
  12. package/cypress.json +4 -0
  13. package/i18n.yml +61 -0
  14. package/init-tests.ts +12 -0
  15. package/jest.config.js +4 -0
  16. package/package.json +78 -0
  17. package/post-process.ts +61 -0
  18. package/pre-process.ts +32 -0
  19. package/release.config.js +1 -0
  20. package/rollup.config.js +116 -0
  21. package/src/client.js +18 -0
  22. package/src/components/ActiveIncidents.svelte +79 -0
  23. package/src/components/ActiveScheduled.svelte +96 -0
  24. package/src/components/Graph.svelte +76 -0
  25. package/src/components/History.svelte +84 -0
  26. package/src/components/Incident.svelte +161 -0
  27. package/src/components/Incidents.svelte +83 -0
  28. package/src/components/LiveStatus.svelte +190 -0
  29. package/src/components/Loading.svelte +37 -0
  30. package/src/components/Nav.svelte +88 -0
  31. package/src/components/Scheduled.svelte +72 -0
  32. package/src/components/Summary.svelte +54 -0
  33. package/src/routes/_error.svelte +41 -0
  34. package/src/routes/_layout.svelte +110 -0
  35. package/src/routes/error.svelte +27 -0
  36. package/src/routes/history/[number].svelte +17 -0
  37. package/src/routes/incident/[number].svelte +13 -0
  38. package/src/routes/index.svelte +48 -0
  39. package/src/routes/rate-limit-exceeded.svelte +88 -0
  40. package/src/server.js +25 -0
  41. package/src/service-worker.js +15 -0
  42. package/src/template.html +34 -0
  43. package/src/utils/createOctokit.js +67 -0
  44. package/static/global.css +203 -0
  45. package/static/logo-192.png +0 -0
  46. package/static/logo-512.png +0 -0
  47. package/static/manifest.json +20 -0
  48. package/static/themes/dark.css +26 -0
  49. package/static/themes/light.css +26 -0
  50. package/static/themes/night.css +26 -0
  51. package/static/themes/ocean.css +26 -0
  52. package/tsconfig.json +20 -0
@@ -0,0 +1,96 @@
1
+ <script>
2
+ import Loading from "../components/Loading.svelte";
3
+ import { onMount } from "svelte";
4
+ import config from "../data/config.json";
5
+ import { cachedResponse, createOctokit, handleError } from "../utils/createOctokit";
6
+
7
+ let loading = true;
8
+ const octokit = createOctokit();
9
+ const owner = config.owner;
10
+ const repo = config.repo;
11
+ let incidents = [];
12
+
13
+ onMount(async () => {
14
+ try {
15
+ incidents = (
16
+ await cachedResponse(`scheduled-current-${owner}-${repo}`, () =>
17
+ octokit.issues.listForRepo({
18
+ owner,
19
+ repo,
20
+ state: "open",
21
+ filter: "all",
22
+ sort: "created",
23
+ direction: "desc",
24
+ labels: "maintenance",
25
+ })
26
+ )
27
+ ).data;
28
+ incidents = incidents.map((incident, index) => {
29
+ incident.showHeading =
30
+ index === 0 ||
31
+ new Date(incidents[index - 1].created_at).toLocaleDateString() !==
32
+ new Date(incident.created_at).toLocaleDateString();
33
+ incident.metadata = {};
34
+ if (incident.body.includes("<!--")) {
35
+ const summary = incident.body.split("<!--")[1].split("-->")[0];
36
+ const lines = summary
37
+ .split("\n")
38
+ .filter((i) => i.trim())
39
+ .filter((i) => i.includes(":"));
40
+ lines.forEach((i) => {
41
+ incident.metadata[i.split(/:(.+)/)[0].trim()] = i.split(/:(.+)/)[1].trim();
42
+ });
43
+ }
44
+ return incident;
45
+ });
46
+ } catch (error) {
47
+ handleError(error);
48
+ }
49
+ loading = false;
50
+ });
51
+ </script>
52
+
53
+ <section>
54
+ {#if loading}
55
+ <Loading />
56
+ {:else if incidents.length}
57
+ <h2>{config.i18n.scheduledMaintenance}</h2>
58
+ {#each incidents as incident}
59
+ <article class="degraded degraded-active link">
60
+ <div class="f">
61
+ <div>
62
+ <h4>{incident.title.replace("🛑", "").replace("⚠️", "").trim()}</h4>
63
+ {#if incident.metadata.start && incident.metadata.end}
64
+ <div>
65
+ {(new Date(incident.metadata.start).getTime() < new Date().getTime()
66
+ ? config.i18n.scheduledMaintenanceSummaryStarted
67
+ : config.i18n.scheduledMaintenanceSummaryStarts
68
+ )
69
+ .replace(/\$DATE/g, new Date(incident.metadata.start).toLocaleString(config.i18n.locale))
70
+ .replace(
71
+ /\$DURATION/g,
72
+ Math.floor(
73
+ (new Date(incident.metadata.end).getTime() -
74
+ new Date(incident.metadata.start).getTime()) /
75
+ 60000
76
+ )
77
+ )}
78
+ </div>
79
+ {/if}
80
+ </div>
81
+ <div class="f r">
82
+ <a href={`${config.path}/incident/${incident.number}`}>
83
+ {config.i18n.incidentReport.replace(/\$NUMBER/g, incident.number)}
84
+ </a>
85
+ </div>
86
+ </div>
87
+ </article>
88
+ {/each}
89
+ {/if}
90
+ </section>
91
+
92
+ <style>
93
+ section {
94
+ margin-bottom: 2rem;
95
+ }
96
+ </style>
@@ -0,0 +1,76 @@
1
+ <script>
2
+ import Loading from "../components/Loading.svelte";
3
+ import { onMount } from "svelte";
4
+ import config from "../data/config.json";
5
+ import Line from "svelte-chartjs/src/Line.svelte";
6
+ import { cachedResponse, createOctokit, handleError } from "../utils/createOctokit";
7
+
8
+ export let slug;
9
+ let loading = true;
10
+ const octokit = createOctokit();
11
+ const owner = config.owner;
12
+ const repo = config.repo;
13
+ let commits = [];
14
+ let labels = [];
15
+ let data = [];
16
+ let width = 800;
17
+
18
+ onMount(async () => {
19
+ try {
20
+ commits = (
21
+ await cachedResponse(`commits-${owner}-${repo}-${slug}`, () =>
22
+ octokit.repos.listCommits({
23
+ owner,
24
+ repo,
25
+ path: `history/${slug}.yml`,
26
+ per_page: 28,
27
+ })
28
+ )
29
+ ).data.reverse();
30
+ } catch (error) {
31
+ handleError(error);
32
+ }
33
+ commits = commits.map((commit, index) => {
34
+ commit.showHeading =
35
+ index === 0 ||
36
+ new Date(commits[index - 1].created_at).toLocaleDateString() !==
37
+ new Date(commit.created_at).toLocaleDateString();
38
+ return commit;
39
+ });
40
+ data = commits
41
+ .filter((commit) => commit.commit.message.includes("ms) [skip ci]"))
42
+ .map((commit) => parseInt(commit.commit.message.split(" in ")[1].split("ms")[0]));
43
+ labels = commits
44
+ .filter((commit) => commit.commit.message.includes("ms) [skip ci]"))
45
+ .map((commit) => new Date(commit.commit.committer.date).toLocaleString(config.i18n.locale));
46
+ loading = false;
47
+ });
48
+ </script>
49
+
50
+ <section bind:clientWidth={width}>
51
+ {#if loading}
52
+ <Loading />
53
+ {:else if data.length}
54
+ <h2>{config.i18n.sevelDayResponseTime}</h2>
55
+ <Line
56
+ data={{
57
+ labels,
58
+ datasets: [
59
+ {
60
+ label: config.i18n.responseTimeMs,
61
+ backgroundColor: config.graphBackgroundColor || "#89e0cf",
62
+ borderColor: config.graphBorderColor || "#1abc9c",
63
+ data,
64
+ },
65
+ ],
66
+ }}
67
+ {width}
68
+ height={400}
69
+ options={{
70
+ responsive: true,
71
+ maintainAspectRatio: true,
72
+ scales: { xAxes: [{ display: false, gridLines: { display: false } }] },
73
+ }}
74
+ />
75
+ {/if}
76
+ </section>
@@ -0,0 +1,84 @@
1
+ <script>
2
+ import Loading from "../components/Loading.svelte";
3
+ import { onMount } from "svelte";
4
+ import config from "../data/config.json";
5
+ import { cachedResponse, createOctokit, handleError } from "../utils/createOctokit";
6
+
7
+ export let slug;
8
+ let loading = true;
9
+ const octokit = createOctokit();
10
+ const owner = config.owner;
11
+ const repo = config.repo;
12
+ let incidents = [];
13
+
14
+ onMount(async () => {
15
+ try {
16
+ incidents = (
17
+ await cachedResponse(`closed-issues-${owner}-${repo}-${slug}`, () =>
18
+ octokit.issues.listForRepo({
19
+ owner,
20
+ repo,
21
+ state: "closed",
22
+ filter: "all",
23
+ sort: "created",
24
+ direction: "desc",
25
+ labels: `status,${slug}`,
26
+ })
27
+ )
28
+ ).data;
29
+ } catch (error) {
30
+ handleError(error);
31
+ }
32
+ incidents = incidents.map((incident, index) => {
33
+ incident.showHeading =
34
+ index === 0 ||
35
+ new Date(incidents[index - 1].created_at).toLocaleDateString() !==
36
+ new Date(incident.created_at).toLocaleDateString();
37
+ return incident;
38
+ });
39
+ loading = false;
40
+ });
41
+ </script>
42
+
43
+ <style>
44
+ h2 {
45
+ margin-top: 2rem;
46
+ }
47
+ </style>
48
+
49
+ <section>
50
+ {#if loading}
51
+ <Loading />
52
+ {:else if incidents.length}
53
+ <h2>{config.i18n.pastIncidents}</h2>
54
+ {#each incidents as incident}
55
+ {#if incident.showHeading}
56
+ <h3>{new Date(incident.created_at).toLocaleDateString(config.i18n.locale)}</h3>
57
+ {/if}
58
+ <article class="down link {incident.title.includes('degraded') ? 'degraded' : ''}">
59
+ <div class="f">
60
+ <div>
61
+ <h4>{incident.title.replace('🛑', '').replace('⚠️', '').trim()}</h4>
62
+ <div>
63
+ {@html config.i18n.pastIncidentsResolved
64
+ .replace(
65
+ /\$MINUTES/g,
66
+ (
67
+ (new Date(incident.closed_at).getTime() -
68
+ new Date(incident.created_at).getTime()) /
69
+ 60000
70
+ ).toFixed(0)
71
+ )
72
+ .replace(/\$POSTS/g, incident.comments)}
73
+ </div>
74
+ </div>
75
+ <div class="f r">
76
+ <a href={`${config.path}/incident/${incident.number}`}>
77
+ {config.i18n.incidentReport.replace(/\$NUMBER/g, incident.number)}
78
+ </a>
79
+ </div>
80
+ </div>
81
+ </article>
82
+ {/each}
83
+ {/if}
84
+ </section>
@@ -0,0 +1,161 @@
1
+ <script>
2
+ import Loading from "../components/Loading.svelte";
3
+ import { onMount } from "svelte";
4
+ import snarkdown from "snarkdown";
5
+ import config from "../data/config.json";
6
+ import { cachedResponse, createOctokit, handleError } from "../utils/createOctokit";
7
+
8
+ export let number;
9
+
10
+ let md = snarkdown;
11
+ let loading = true;
12
+ let loadingIncident = true;
13
+
14
+ const octokit = createOctokit();
15
+ const owner = config.owner;
16
+ const repo = config.repo;
17
+ let comments = [];
18
+ let incident = {};
19
+
20
+ onMount(async () => {
21
+ try {
22
+ incident = (
23
+ await cachedResponse(`issue-${owner}-${repo}-${number}`, () =>
24
+ octokit.issues.get({
25
+ owner,
26
+ repo,
27
+ issue_number: number,
28
+ sort: "created",
29
+ direction: "desc",
30
+ })
31
+ )
32
+ ).data;
33
+ incident.metadata = {};
34
+ if (incident.body.includes("<!--")) {
35
+ const summary = incident.body.split("<!--")[1].split("-->")[0];
36
+ const lines = summary
37
+ .split("\n")
38
+ .filter((i) => i.trim())
39
+ .filter((i) => i.includes(":"));
40
+ lines.forEach((i) => {
41
+ incident.metadata[i.split(/:(.+)/)[0].trim()] = i.split(/:(.+)/)[1].trim();
42
+ });
43
+ }
44
+ } catch (error) {
45
+ handleError(error);
46
+ }
47
+ loadingIncident = false;
48
+ try {
49
+ comments = (
50
+ await cachedResponse(`issue-comments-${owner}-${repo}-${number}`, () =>
51
+ octokit.issues.listComments({
52
+ owner,
53
+ repo,
54
+ issue_number: number,
55
+ })
56
+ )
57
+ ).data.reverse();
58
+ } catch (error) {
59
+ handleError(error);
60
+ }
61
+ loading = false;
62
+ });
63
+ </script>
64
+
65
+ <svelte:head>
66
+ <title>{config.i18n.incidentTitle.replace("$NUMBER", number)}</title>
67
+ </svelte:head>
68
+
69
+ <h2>
70
+ {#if loadingIncident}
71
+ {config.i18n.incidentDetails}
72
+ {:else}
73
+ {incident.title}
74
+ <span class={`tag ${incident.state}`}>
75
+ {incident.state === "closed"
76
+ ? incident.metadata.start
77
+ ? config.i18n.incidentCompleted
78
+ : config.i18n.incidentFixed
79
+ : incident.metadata.start
80
+ ? config.i18n.incidentScheduled
81
+ : config.i18n.incidentOngoing}
82
+ </span>
83
+ {/if}
84
+ </h2>
85
+
86
+ <section>
87
+ {#if loading}
88
+ <Loading />
89
+ {:else}
90
+ <div class="f">
91
+ <dl>
92
+ {#if incident.metadata.start}
93
+ <dt>
94
+ {new Date(incident.metadata.start).getTime() < new Date().getTime()
95
+ ? config.i18n.startedAt
96
+ : config.i18n.startsAt}
97
+ </dt>
98
+ <dd>{new Date(incident.metadata.start).toLocaleString(config.i18n.locale)}</dd>
99
+ {:else}
100
+ <dt>{config.i18n.incidentOpenedAt}</dt>
101
+ <dd>{new Date(incident.created_at).toLocaleString(config.i18n.locale)}</dd>
102
+ {/if}
103
+ {#if incident.metadata.start && incident.metadata.end}
104
+ <dt>{config.i18n.duration}</dt>
105
+ <dd>
106
+ {config.i18n.durationMin.replace(
107
+ /\$DURATION/g,
108
+ Math.floor(
109
+ (new Date(incident.metadata.end).getTime() -
110
+ new Date(incident.metadata.start).getTime()) /
111
+ 60000
112
+ )
113
+ )}
114
+ </dd>
115
+ {:else if incident.closed_at}
116
+ <dt>{config.i18n.incidentClosedAt}</dt>
117
+ <dd>{new Date(incident.closed_at).toLocaleString(config.i18n.locale)}</dd>
118
+ {/if}
119
+ </dl>
120
+ <div class="r">
121
+ <p>
122
+ <a href={`https://github.com/${config.owner}/${config.repo}/issues/${number}`}>
123
+ {config.i18n.incidentViewOnGitHub}
124
+ </a>
125
+ </p>
126
+ </div>
127
+ </div>
128
+ {#each comments as comment}
129
+ <article>
130
+ <p>
131
+ {@html md(comment.body)}
132
+ </p>
133
+ <div>
134
+ {@html config.i18n.incidentCommentSummary
135
+ .replace(
136
+ /\$DATE/g,
137
+ `<a href=${comment.html_url}>${new Date(comment.created_at).toLocaleString(config.i18n.locale)}</a>`
138
+ )
139
+ .replace(/\$AUTHOR/g, `<a href=${comment.user.html_url}>@${comment.user.login}</a>`)}
140
+ </div>
141
+ </article>
142
+ {/each}
143
+ {/if}
144
+ </section>
145
+
146
+ <footer><a href={config.path}>{config.i18n.incidentBack}</a></footer>
147
+
148
+ <style>
149
+ footer {
150
+ margin-top: 2rem;
151
+ }
152
+ p {
153
+ margin-top: 0;
154
+ }
155
+ h2 {
156
+ line-height: 1;
157
+ }
158
+ .r {
159
+ text-align: right;
160
+ }
161
+ </style>
@@ -0,0 +1,83 @@
1
+ <script>
2
+ import Loading from "../components/Loading.svelte";
3
+ import { onMount } from "svelte";
4
+ import config from "../data/config.json";
5
+ import { cachedResponse, createOctokit, handleError } from "../utils/createOctokit";
6
+
7
+ let loading = true;
8
+ const octokit = createOctokit();
9
+ const owner = config.owner;
10
+ const repo = config.repo;
11
+ let incidents = [];
12
+
13
+ onMount(async () => {
14
+ try {
15
+ incidents = (
16
+ await cachedResponse(`closed-issues-${owner}-${repo}`, () =>
17
+ octokit.issues.listForRepo({
18
+ owner,
19
+ repo,
20
+ state: "closed",
21
+ filter: "all",
22
+ sort: "created",
23
+ direction: "desc",
24
+ labels: "status",
25
+ })
26
+ )
27
+ ).data;
28
+ } catch (error) {
29
+ handleError(error);
30
+ }
31
+ incidents = incidents.map((incident, index) => {
32
+ incident.showHeading =
33
+ index === 0 ||
34
+ new Date(incidents[index - 1].created_at).toLocaleDateString() !==
35
+ new Date(incident.created_at).toLocaleDateString();
36
+ return incident;
37
+ });
38
+ loading = false;
39
+ });
40
+ </script>
41
+
42
+ <style>
43
+ h2 {
44
+ margin-top: 2rem;
45
+ }
46
+ </style>
47
+
48
+ <section>
49
+ {#if loading}
50
+ <Loading />
51
+ {:else if incidents.length}
52
+ <h2>{config.i18n.pastIncidents}</h2>
53
+ {#each incidents as incident}
54
+ {#if incident.showHeading}
55
+ <h3>{new Date(incident.created_at).toLocaleDateString(config.i18n.locale)}</h3>
56
+ {/if}
57
+ <article class="down link {incident.title.includes('degraded') ? 'degraded' : ''}">
58
+ <div class="f">
59
+ <div>
60
+ <h4>{incident.title.replace('🛑', '').replace('⚠️', '').trim()}</h4>
61
+ <div>
62
+ {@html config.i18n.pastIncidentsResolved
63
+ .replace(
64
+ /\$MINUTES/g,
65
+ (
66
+ (new Date(incident.closed_at).getTime() -
67
+ new Date(incident.created_at).getTime()) /
68
+ 60000
69
+ ).toFixed(0)
70
+ )
71
+ .replace(/\$POSTS/g, incident.comments)}
72
+ </div>
73
+ </div>
74
+ <div class="f r">
75
+ <a href={`${config.path}/incident/${incident.number}`}>
76
+ {config.i18n.incidentReport.replace(/\$NUMBER/g, incident.number)}
77
+ </a>
78
+ </div>
79
+ </div>
80
+ </article>
81
+ {/each}
82
+ {/if}
83
+ </section>
@@ -0,0 +1,190 @@
1
+ <script>
2
+ import Loading from "../components/Loading.svelte";
3
+ import { onMount } from "svelte";
4
+ import config from "../data/config.json";
5
+ import { createOctokit, handleError } from "../utils/createOctokit";
6
+
7
+ let loading = true;
8
+ const octokit = createOctokit();
9
+ const owner = config.owner;
10
+ const repo = config.repo;
11
+ let sites = [];
12
+
13
+ let { apiBaseUrl, userContentBaseUrl } = config["status-website"] || {};
14
+ if (!apiBaseUrl) apiBaseUrl = "https://api.github.com";
15
+ if (!userContentBaseUrl) userContentBaseUrl = "https://raw.githubusercontent.com";
16
+
17
+ const graphsBaseUrl = `${userContentBaseUrl}/${owner}/${repo}/master/graphs`;
18
+ let form = null;
19
+
20
+ let selected = "week";
21
+
22
+ onMount(async () => {
23
+ try {
24
+ const res = await fetch(`${userContentBaseUrl}/${owner}/${repo}/master/history/summary.json`);
25
+ sites = await res.json();
26
+ } catch (error) {
27
+ handleError(error);
28
+ }
29
+ loading = false;
30
+ if (form) form.classList.remove("changed");
31
+ });
32
+
33
+ const changed = () => {
34
+ if (form) {
35
+ form.classList.add("changed");
36
+ setTimeout(() => form.classList.remove("changed"), 500);
37
+ }
38
+ };
39
+ </script>
40
+
41
+ <div class="f changed" bind:this={form}>
42
+ <h2>{config.i18n.liveStatus}</h2>
43
+ <form class="f r">
44
+ <div>
45
+ <input
46
+ value="day"
47
+ bind:group={selected}
48
+ name="d"
49
+ type="radio"
50
+ on:change={changed}
51
+ id="data_day"
52
+ /><label for="data_day">{config.i18n.duration24H}</label>
53
+ </div>
54
+ <div>
55
+ <input
56
+ value="week"
57
+ bind:group={selected}
58
+ name="d"
59
+ type="radio"
60
+ on:change={changed}
61
+ id="data_week"
62
+ /><label for="data_week">{config.i18n.duration7D}</label>
63
+ </div>
64
+ <div>
65
+ <input
66
+ value="month"
67
+ bind:group={selected}
68
+ name="d"
69
+ type="radio"
70
+ on:change={changed}
71
+ id="data_month"
72
+ /><label for="data_month">{config.i18n.duration30D}</label>
73
+ </div>
74
+ <div>
75
+ <input
76
+ value="year"
77
+ bind:group={selected}
78
+ name="d"
79
+ type="radio"
80
+ on:change={changed}
81
+ id="data_year"
82
+ /><label for="data_year">{config.i18n.duration1Y}</label>
83
+ </div>
84
+ <div>
85
+ <input
86
+ value="all"
87
+ bind:group={selected}
88
+ name="d"
89
+ type="radio"
90
+ on:change={changed}
91
+ id="data_all"
92
+ /><label for="data_all">{config.i18n.durationAll}</label>
93
+ </div>
94
+ </form>
95
+ </div>
96
+ <section class="live-status">
97
+ {#if loading}
98
+ <Loading />
99
+ {:else if sites.length}
100
+ {#each sites as site}
101
+ <article
102
+ class={`${site.status} link graph`}
103
+ style="--background: url('{`${graphsBaseUrl}/${site.slug}/response-time${
104
+ selected === "day"
105
+ ? "-day"
106
+ : selected === "week"
107
+ ? "-week"
108
+ : selected === "month"
109
+ ? "-month"
110
+ : selected === "year"
111
+ ? "-year"
112
+ : ""
113
+ }.png`}')"
114
+ ><h4>
115
+ <img class="icon" alt="" src={site.icon} />
116
+ <a href={`${config.path}/history/${site.slug}`}>{site.name}</a>
117
+ </h4>
118
+ <div>
119
+ {@html config.i18n.overallUptime.split("$UPTIME")[0]}
120
+ <span class="data"
121
+ >{selected === "day"
122
+ ? site.uptimeDay
123
+ : selected === "week"
124
+ ? site.uptimeWeek
125
+ : selected === "month"
126
+ ? site.uptimeMonth
127
+ : selected === "year"
128
+ ? site.uptimeYear
129
+ : site.uptime}
130
+ {@html config.i18n.overallUptime.split("$UPTIME")[1]}</span
131
+ >
132
+ </div>
133
+ {#if site.showAverageResponseTime === undefined || site.showAverageResponseTime}
134
+ <div>
135
+ {@html config.i18n.averageResponseTime.split("$TIME")[0]}
136
+ <span class="data"
137
+ >{selected === "day"
138
+ ? site.timeDay
139
+ : selected === "week"
140
+ ? site.timeWeek
141
+ : selected === "month"
142
+ ? site.timeMonth
143
+ : selected === "year"
144
+ ? site.timeYear
145
+ : site.time}
146
+ {@html config.i18n.averageResponseTime.split("$TIME")[1]}</span
147
+ >
148
+ </div>
149
+ {/if}
150
+ </article>
151
+ {/each}
152
+ {/if}
153
+ </section>
154
+
155
+ <style>
156
+ article.graph {
157
+ background-image: var(--background);
158
+ background-size: contain;
159
+ background-repeat: no-repeat;
160
+ background-position: center right;
161
+ }
162
+ .icon {
163
+ height: 1rem;
164
+ margin-right: 0.33rem;
165
+ vertical-align: middle;
166
+ transform: scale(1.1) translateY(-0.1rem);
167
+ }
168
+ a {
169
+ text-decoration: none;
170
+ }
171
+ .r input:checked + label {
172
+ font-weight: bold;
173
+ }
174
+ .r input {
175
+ display: none;
176
+ }
177
+ .r label {
178
+ margin-left: 1rem;
179
+ }
180
+ .data {
181
+ transition: 0.3s;
182
+ }
183
+ .changed + section {
184
+ background-color: transparent;
185
+ }
186
+ .data {
187
+ padding: 0.15rem 0.25rem;
188
+ border-radius: 0.2rem;
189
+ }
190
+ </style>