Merge pull request #14 from koj-co/feature-sapper-site

Feature sapper site
This commit is contained in:
Anand Chowdhary
2020-08-10 13:02:17 +05:30
committed by GitHub
28 changed files with 3528 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
.DS_Store
/node_modules/
/src/node_modules/@sapper/
yarn-error.log
/cypress/screenshots/
/__sapper__/
+4
View File
@@ -0,0 +1,4 @@
{
"baseUrl": "http://localhost:3000",
"video": false
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
+19
View File
@@ -0,0 +1,19 @@
describe('Sapper template app', () => {
beforeEach(() => {
cy.visit('/')
});
it('has the correct <h1>', () => {
cy.contains('h1', 'Great success!')
});
it('navigates to /about', () => {
cy.get('nav a').contains('about').click();
cy.url().should('include', '/about');
});
it('navigates to /blog', () => {
cy.get('nav a').contains('blog').click();
cy.url().should('include', '/blog');
});
});
+17
View File
@@ -0,0 +1,17 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
+25
View File
@@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
+20
View File
@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
+2569
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
{
"name": "koj-status-app",
"version": "0.0.0",
"scripts": {
"dev": "sapper dev",
"build": "sapper build --legacy",
"export": "sapper export --legacy",
"start": "node __sapper__/build",
"cy:run": "cypress run",
"cy:open": "cypress open",
"test": "run-p --race dev cy:run"
},
"dependencies": {
"@octokit/rest": "^18.0.3",
"compression": "^1.7.1",
"polka": "next",
"sirv": "^1.0.0",
"snarkdown": "^1.2.2"
},
"devDependencies": {
"npm-run-all": "^4.1.5",
"sapper": "^0.28.0",
"svelte": "^3.17.3",
"@babel/core": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/runtime": "^7.0.0",
"@rollup/plugin-babel": "^5.0.0",
"@rollup/plugin-commonjs": "^14.0.0",
"@rollup/plugin-node-resolve": "^8.0.0",
"@rollup/plugin-replace": "^2.2.0",
"rollup": "^2.3.4",
"rollup-plugin-svelte": "^5.0.1",
"rollup-plugin-terser": "^6.1.0"
}
}
+116
View File
@@ -0,0 +1,116 @@
import resolve from "@rollup/plugin-node-resolve";
import replace from "@rollup/plugin-replace";
import commonjs from "@rollup/plugin-commonjs";
import svelte from "rollup-plugin-svelte";
import babel from "@rollup/plugin-babel";
import { terser } from "rollup-plugin-terser";
import config from "sapper/config/rollup.js";
import pkg from "./package.json";
const mode = process.env.NODE_ENV;
const dev = mode === "development";
const legacy = !!process.env.SAPPER_LEGACY_BUILD;
const onwarn = (warning, onwarn) =>
(warning.code === "MISSING_EXPORT" && /'preload'/.test(warning.message)) ||
(warning.code === "CIRCULAR_DEPENDENCY" &&
/[/\\]@sapper[/\\]/.test(warning.message)) ||
onwarn(warning);
export default {
client: {
input: config.client.input(),
output: config.client.output(),
plugins: [
replace({
"process.browser": true,
"process.env.NODE_ENV": JSON.stringify(mode),
}),
svelte({
dev,
hydratable: true,
emitCss: true,
}),
resolve({
browser: true,
dedupe: ["svelte"],
}),
commonjs(),
legacy &&
babel({
extensions: [".js", ".mjs", ".html", ".svelte"],
babelHelpers: "runtime",
exclude: ["node_modules/@babel/**"],
presets: [
[
"@babel/preset-env",
{
targets: "> 0.25%, not dead",
},
],
],
plugins: [
"@babel/plugin-syntax-dynamic-import",
[
"@babel/plugin-transform-runtime",
{
useESModules: true,
},
],
],
}),
!dev &&
terser({
module: true,
}),
],
preserveEntrySignatures: false,
onwarn,
},
server: {
input: config.server.input(),
output: config.server.output(),
plugins: [
replace({
"process.browser": false,
"process.env.NODE_ENV": JSON.stringify(mode),
}),
svelte({
generate: "ssr",
hydratable: true,
dev,
}),
resolve({
dedupe: ["svelte"],
}),
commonjs(),
],
external: Object.keys(pkg.dependencies).concat(
require("module").builtinModules
),
preserveEntrySignatures: "strict",
onwarn,
},
serviceworker: {
input: config.serviceworker.input(),
output: config.serviceworker.output(),
plugins: [
resolve(),
replace({
"process.browser": true,
"process.env.NODE_ENV": JSON.stringify(mode),
}),
commonjs(),
!dev && terser(),
],
preserveEntrySignatures: false,
onwarn,
},
};
+5
View File
@@ -0,0 +1,5 @@
import * as sapper from '@sapper/app';
sapper.start({
target: document.querySelector('#sapper')
});
@@ -0,0 +1,73 @@
<script>
import Loading from "../components/Loading.svelte";
import { Octokit } from "@octokit/rest";
import { onMount } from "svelte";
let loading = true;
const octokit = new Octokit({
userAgent: "KojBot",
});
const owner = "koj-co";
const repo = "status";
let incidents = [];
onMount(async () => {
incidents = (
await octokit.issues.listForRepo({
owner,
repo,
state: "open",
filter: "all",
sort: "created",
direction: "desc",
labels: "status",
})
).data;
incidents = incidents.map((incident, index) => {
incident.showHeading =
index === 0 ||
new Date(incidents[index - 1].created_at).toLocaleDateString() !==
new Date(incident.created_at).toLocaleDateString();
return incident;
});
loading = false;
});
</script>
<style>
article {
background-color: #ffdce3;
}
article.good {
background-color: #dcffeb;
color: #003300;
}
</style>
{#if !incidents.length && !loading}
<article class="good">&nbsp; All systems are operational</article>
{/if}
<section>
{#if loading}
<Loading />
{:else if incidents.length}
{#each incidents as incident}
<h2>Active Incidents</h2>
<article class="down">
<div class="f">
<div>
<h4>{incident.title.replace('🛑', '').replace('⚠️', '').trim()}</h4>
<div>
Opened at {new Date(incident.created_at).toLocaleString()} with {incident.comments}
posts
</div>
</div>
<div class="f r">
<a href={`/${incident.number}`}>Incident report &rarr;</a>
</div>
</div>
</article>
{/each}
{/if}
</section>
+145
View File
@@ -0,0 +1,145 @@
<script>
import Loading from "../components/Loading.svelte";
import { Octokit } from "@octokit/rest";
import { onMount } from "svelte";
import snarkdown from "snarkdown";
export let number;
let md = snarkdown;
let loading = true;
let loadingIncident = true;
const octokit = new Octokit({
userAgent: "KojBot",
});
const owner = "koj-co";
const repo = "status";
let comments = [];
let incident = {};
onMount(async () => {
incident = (
await octokit.issues.get({
owner,
repo,
issue_number: number,
sort: "created",
direction: "desc",
})
).data;
loadingIncident = false;
comments = (
await octokit.issues.listComments({
owner,
repo,
issue_number: number,
})
).data.reverse();
loading = false;
});
</script>
<style>
footer {
margin-top: 2rem;
}
dl {
margin-bottom: 1.5rem;
}
dl dt {
font-weight: bold;
}
dl dd {
margin: 0 0 1rem 0;
}
p {
margin-top: 0;
}
h2 {
line-height: 1;
}
.closed {
background-color: #16a085;
}
.open {
background-color: #c0392b;
}
.closed,
.open {
display: inline-block;
color: #fff;
padding: 0.33rem 0.5rem;
border-radius: 0.2rem;
font-size: 70%;
vertical-align: middle;
margin-top: -0.2rem;
margin-left: 0.5rem;
}
.r {
text-align: right;
}
</style>
<svelte:head>
<title>Incident #{number} Details</title>
</svelte:head>
<h2>
{#if loadingIncident}
Incident Details
{:else}
{incident.title}
<span class={incident.state}>
{incident.state === 'closed' ? 'Fixed' : 'Ongoing'}
</span>
{/if}
</h2>
<section>
{#if loading}
<Loading />
{:else}
<div class="f">
<dl>
<dt>Opened at</dt>
<dd>{new Date(incident.created_at).toLocaleString()}</dd>
<dt>Closed at</dt>
{#if incident.closed_at}
<dd>{new Date(incident.closed_at).toLocaleString()}</dd>
{/if}
</dl>
<div class="r">
<p>
<a href={`https://github.com/koj-co/status/issues/${number}`}>
Subscribe to Updates
</a>
</p>
<p>
<a href={`https://github.com/koj-co/status/issues/${number}`}>
View on GitHub
</a>
</p>
</div>
</div>
{#each comments as comment}
<article>
<p>
{@html md(comment.body)}
</p>
<div>
Posted at
<a href={comment.html_url}>
{new Date(comment.created_at).toLocaleString()}
</a>
by
<a href={comment.user.html_url}>@{comment.user.login}</a>
</div>
</article>
{/each}
{/if}
</section>
<footer>
<a href="/">&larr; Back to all comments</a>
</footer>
+63
View File
@@ -0,0 +1,63 @@
<script>
import Loading from "../components/Loading.svelte";
import { Octokit } from "@octokit/rest";
import { onMount } from "svelte";
let loading = true;
const octokit = new Octokit({
userAgent: "KojBot",
});
const owner = "koj-co";
const repo = "status";
let incidents = [];
onMount(async () => {
incidents = (
await octokit.issues.listForRepo({
owner,
repo,
state: "closed",
filter: "all",
sort: "created",
direction: "desc",
labels: "status",
})
).data;
incidents = incidents.map((incident, index) => {
incident.showHeading =
index === 0 ||
new Date(incidents[index - 1].created_at).toLocaleDateString() !==
new Date(incident.created_at).toLocaleDateString();
return incident;
});
loading = false;
});
</script>
<h2>Past Incidents</h2>
<section>
{#if loading}
<Loading />
{:else}
{#each incidents as incident}
{#if incident.showHeading}
<h3>{new Date(incident.created_at).toLocaleDateString()}</h3>
{/if}
<article class="down">
<div class="f">
<div>
<h4>{incident.title.replace('🛑', '').replace('⚠️', '').trim()}</h4>
<div>
Resolved in {((new Date(incident.closed_at).getTime() - new Date(incident.created_at).getTime()) / 60000).toFixed(0)}
minutes with {incident.comments} posts
</div>
</div>
<div class="f r">
<a href={`/${incident.number}`}>Incident report &rarr;</a>
</div>
</div>
</article>
{/each}
{/if}
</section>
+28
View File
@@ -0,0 +1,28 @@
<style>
.loading {
text-align: center;
margin: 2.5rem 0;
}
svg {
zoom: 1.5;
}
</style>
<div class="loading">
<svg width="38" height="38" xmlns="http://www.w3.org/2000/svg" stroke="#aaa">
<g fill="none" fill-rule="evenodd">
<g transform="translate(1 1)" stroke-width="2">
<circle stroke-opacity=".5" cx="18" cy="18" r="18" />
<path d="M36 18c0-9.94-8.06-18-18-18">
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="1s"
repeatCount="indefinite" />
</path>
</g>
</g>
</svg>
</div>
+71
View File
@@ -0,0 +1,71 @@
<script>
export let segment;
</script>
<style>
nav {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
font-weight: 300;
padding: 0 1em;
background-color: #fff;
margin-bottom: 2rem;
}
ul {
margin: 0;
padding: 0;
}
ul::after {
content: "";
display: block;
clear: both;
}
li {
display: block;
float: left;
}
[aria-current] {
border-bottom: 2px solid #3498db;
}
a {
text-decoration: none;
padding: 1.5rem 2rem;
display: block;
}
.logo {
float: left;
height: 3.33rem;
margin-top: 0.75rem;
margin-right: 2rem;
border-radius: 0.2rem;
}
</style>
<nav>
<div class="container">
<img
class="logo"
alt="Koj"
src="https://avatars1.githubusercontent.com/u/65495851?s=200&v=4" />
<ul>
<li>
<a aria-current={segment === undefined ? 'page' : undefined} href=".">
Status
</a>
</li>
<li>
<a aria-current={segment === 'about' ? 'page' : undefined} href="about">
About
</a>
</li>
<li>
<a href="/">GitHub</a>
</li>
</ul>
</div>
</nav>
+13
View File
@@ -0,0 +1,13 @@
<script context="module">
export async function preload(page) {
const { number } = page.params;
return { number };
}
</script>
<script>
import Incident from "../components/Incident.svelte";
export let number;
</script>
<Incident {number} />
+41
View File
@@ -0,0 +1,41 @@
<script>
export let status;
export let error;
const dev = process.env.NODE_ENV === "development";
</script>
<style>
h1,
p {
margin: 0 auto;
}
h1 {
font-size: 2.8em;
font-weight: 700;
margin: 0 0 0.5em 0;
}
p {
margin: 1em auto;
}
@media (min-width: 480px) {
h1 {
font-size: 4em;
}
}
</style>
<svelte:head>
<title>{status}</title>
</svelte:head>
<h1>{status}</h1>
<p>{error.message}</p>
{#if dev && error.stack}
<pre>{error.stack}</pre>
{/if}
+11
View File
@@ -0,0 +1,11 @@
<script>
import Nav from "../components/Nav.svelte";
export let segment;
</script>
<Nav {segment} />
<main class="container">
<slot />
</main>
+13
View File
@@ -0,0 +1,13 @@
<svelte:head>
<title>About</title>
</svelte:head>
<h1>About this site</h1>
<p>
This site is built with
<a href="/">Svelte</a>
using
<a href="/">Sapper</a>
, and uses the official GitHub API to fetch data from the status repository.
</p>
+11
View File
@@ -0,0 +1,11 @@
<script>
import ActiveIncidents from "../components/ActiveIncidents.svelte";
import Incidents from "../components/Incidents.svelte";
</script>
<svelte:head>
<title>Koj Status</title>
</svelte:head>
<ActiveIncidents />
<Incidents />
+17
View File
@@ -0,0 +1,17 @@
import sirv from 'sirv';
import polka from 'polka';
import compression from 'compression';
import * as sapper from '@sapper/server';
const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';
polka() // You can also use Express
.use(
compression({ threshold: 0 }),
sirv('static', { dev }),
sapper.middleware()
)
.listen(PORT, err => {
if (err) console.log('error', err);
});
+82
View File
@@ -0,0 +1,82 @@
import { timestamp, files, shell, routes } from '@sapper/service-worker';
const ASSETS = `cache${timestamp}`;
// `shell` is an array of all the files generated by the bundler,
// `files` is an array of everything in the `static` directory
const to_cache = shell.concat(files);
const cached = new Set(to_cache);
self.addEventListener('install', event => {
event.waitUntil(
caches
.open(ASSETS)
.then(cache => cache.addAll(to_cache))
.then(() => {
self.skipWaiting();
})
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(async keys => {
// delete old caches
for (const key of keys) {
if (key !== ASSETS) await caches.delete(key);
}
self.clients.claim();
})
);
});
self.addEventListener('fetch', event => {
if (event.request.method !== 'GET' || event.request.headers.has('range')) return;
const url = new URL(event.request.url);
// don't try to handle e.g. data: URIs
if (!url.protocol.startsWith('http')) return;
// ignore dev server requests
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
// always serve static files and bundler-generated assets from cache
if (url.host === self.location.host && cached.has(url.pathname)) {
event.respondWith(caches.match(event.request));
return;
}
// for pages, you might want to serve a shell `service-worker-index.html` file,
// which Sapper has generated for you. It's not right for every
// app, but if it's right for yours then uncomment this section
/*
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
event.respondWith(caches.match('/service-worker-index.html'));
return;
}
*/
if (event.request.cache === 'only-if-cached') return;
// for everything else, try the network first, falling back to
// cache if the user is offline. (If the pages never change, you
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open(`offline${timestamp}`)
.then(async cache => {
try {
const response = await fetch(event.request);
cache.put(event.request, response.clone());
return response;
} catch(err) {
const response = await cache.match(event.request);
if (response) return response;
throw err;
}
})
);
});
+33
View File
@@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="theme-color" content="#333333">
%sapper.base%
<link rel="stylesheet" href="global.css">
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
<link rel="icon" type="image/png" href="favicon.png">
<!-- Sapper creates a <script> tag containing `src/client.js`
and anything else it needs to hydrate the app and
initialise the router -->
%sapper.scripts%
<!-- Sapper generates a <style> tag containing critical CSS
for the current page. CSS for the rest of the app is
lazily loaded when it precaches secondary pages -->
%sapper.styles%
<!-- This contains the contents of the <svelte:head> component, if
the current page has one -->
%sapper.head%
</head>
<body>
<!-- The application will be rendered inside this element,
because `src/client.js` references it -->
<div id="sapper">%sapper.html%</div>
</body>
</html>
+84
View File
@@ -0,0 +1,84 @@
body {
margin: 0;
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
line-height: 1.5;
padding-bottom: 2.5rem;
color: #333;
background-color: #f0f7f7;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0 0 0.5em 0;
font-weight: 400;
line-height: 1.2;
}
h1 {
font-size: 2em;
}
a {
color: inherit;
}
code {
font-family: menlo, inconsolata, monospace;
font-size: calc(1em - 2px);
color: #555;
background-color: #f0f0f0;
padding: 0.2em 0.4em;
border-radius: 2px;
}
@media (min-width: 400px) {
body {
font-size: 16px;
}
}
.container {
max-width: 800px;
margin: 0 auto;
}
article {
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.1);
padding: 1rem 1.25rem;
border-radius: 0.2rem;
}
article + article {
margin-top: 0.5rem;
}
article + h3 {
margin-top: 1rem;
}
h3 {
opacity: 0.75;
}
h4 {
font-weight: bold;
}
article.down {
border-left: 0.2rem solid #e74c3c;
}
article.up {
border-left: 0.2rem solid #2ecc71;
}
.f {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
section + h2 {
margin-top: 2rem;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+20
View File
@@ -0,0 +1,20 @@
{
"background_color": "#ffffff",
"theme_color": "#333333",
"name": "TODO",
"short_name": "TODO",
"display": "minimal-ui",
"start_url": "/",
"icons": [
{
"src": "logo-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "logo-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}