Browse Source

⚡️❤️ Updated with Glitch

master
Glitch (steady-sundial) 1 year ago
parent
commit
bbc2aba6b2
16 changed files with 2433 additions and 2 deletions
  1. +5
    -0
      .glitch-assets
  2. +27
    -2
      README.md
  3. +103
    -0
      lib/indieauth-authn.js
  4. +34
    -0
      package.json
  5. +4
    -0
      public/client.js
  6. +65
    -0
      public/images/indiewebcamp-logo-lockup-color.svg
  7. +71
    -0
      public/style.css
  8. +2
    -0
      robots.txt
  9. +224
    -0
      server.js
  10. +1739
    -0
      shrinkwrap.yaml
  11. +27
    -0
      views/dashboard.hbs
  12. +10
    -0
      views/index.hbs
  13. +57
    -0
      views/layouts/main.hbs
  14. +21
    -0
      views/partials/checks-result.hbs
  15. +39
    -0
      views/partials/checks.hbs
  16. +5
    -0
      views/partials/sign-in-form.hbs

+ 5
- 0
.glitch-assets View File

@@ -0,0 +1,5 @@
{"name":"drag-in-files.svg","date":"2016-10-22T16:17:49.954Z","url":"https://cdn.hyperdev.com/drag-in-files.svg","type":"image/svg","size":7646,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/drag-in-files.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(102, 153, 205)","uuid":"adSBq97hhhpFNUna"}
{"name":"click-me.svg","date":"2016-10-23T16:17:49.954Z","url":"https://cdn.hyperdev.com/click-me.svg","type":"image/svg","size":7116,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/click-me.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(243, 185, 186)","uuid":"adSBq97hhhpFNUnb"}
{"name":"paste-me.svg","date":"2016-10-24T16:17:49.954Z","url":"https://cdn.hyperdev.com/paste-me.svg","type":"image/svg","size":7242,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/paste-me.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(42, 179, 185)","uuid":"adSBq97hhhpFNUnc"}
{"uuid":"adSBq97hhhpFNUna","deleted":true}
{"name":"indiewebcamp-logo-lockup-color.svg","date":"2018-06-27T23:48:23.424Z","url":"https://cdn.glitch.com/6e8ff9d0-2467-4804-a6d0-638ce5ab6028%2Findiewebcamp-logo-lockup-color.svg","type":"image/svg+xml","size":3284,"imageWidth":300,"imageHeight":143,"thumbnail":"https://cdn.glitch.com/6e8ff9d0-2467-4804-a6d0-638ce5ab6028%2Findiewebcamp-logo-lockup-color.svg","thumbnailWidth":300,"thumbnailHeight":143,"dominantColor":null,"uuid":"xtPvP8TphnQRSHnp"}

+ 27
- 2
README.md View File

@@ -1,2 +1,27 @@
# indiewebring.ws
Code for the glitch.com-powered site at 🕸💍.ws
Welcome to Glitch
=================

Click `Show` in the header to see your app live. Updates to your code will instantly deploy and update live.

**Glitch** is the friendly community where you'll build the app of your dreams. Glitch lets you instantly create, remix, edit, and host an app, bot or site, and you can invite collaborators or helpers to simultaneously edit code with you.

Find out more [about Glitch](https://glitch.com/about).


Your Project
------------

On the front-end,
- edit `public/client.js`, `public/style.css` and `views/index.html`
- drag in `assets`, like images or music, to add them to your project

On the back-end,
- your app starts at `server.js`
- add frameworks and packages in `package.json`
- safely store app secrets in `.env` (nobody can see this but you and people you invite)


Made by [Fog Creek](https://fogcreek.com/)
-------------------

\ ゜o゜)ノ

+ 103
- 0
lib/indieauth-authn.js View File

@@ -0,0 +1,103 @@
/**
* An Express middleware for IndieAuth authentication clients!
* Requires a req.session.
* Stores auth state during the round trip to the auth server (req.session.state and req.session.csrfSecret).
* After handling a successful return from the auth server,
* data about the logged in user are in req.session.user:
* { me: http://example.com }
*/

const express = require('express')
const IndieAuthentication = require('indieauth-authentication')
const crypto = require('crypto')
const qs = require('qs')
const qsStringify = qs.stringify

module.exports = function (options) {
const opts = Object.assign(
{
clientId: 'https://' + process.env.PROJECT_DOMAIN + '.glitch.me',
authStartPath: '/signin',
indieAuthHandlerPath: '/indieauthhandler',
successRedirect: '/dashboard'
},
options
)

const rtr = express.Router()
// Begin the IndieAuth dance. Looks for request.body.me for the user's URL.
rtr.post(opts.authStartPath, (request, response) => {
// console.log(request.body)
request.session.csrfSecret = crypto.randomBytes(32).toString('hex')
if(request.body.returnTo) {
request.session.returnTo = request.body.returnTo;
} else {
delete request.session.returnTo;
}
delete request.session.user
const indieauthn = new IndieAuthentication({
clientId: opts.clientId,
redirectUri: 'https://' + (process.env.MAIN_URL) + opts.indieAuthHandlerPath,
me: request.body.me,
state: request.session.csrfSecret,
})
indieauthn.getAuthUrl()
.then((url) => {
request.session.state = {
me: indieauthn.options.me,
authEndpoint: indieauthn.options.authEndpoint
}
response.redirect(url)
})
.catch((err) => {
// couldn't find auth URL. try falling back to indielogin.
console.log(err)
let redirect_url = "https://indielogin.com/auth?" + qsStringify({
me: request.body.me,
client_id: opts.clientId,
redirect_uri: 'https://' + (process.env.MAIN_URL) + opts.indieAuthHandlerPath,
state: request.session.csrfSecret
})
request.session.state = {
me: indieauthn.options.me,
authEndpoint: 'https://indielogin.com/auth'
}
response.redirect(redirect_url)
})
})
// Handle the return from the authorization endpoint.
// Verify auth code, store user data in req.session.
rtr.get(opts.indieAuthHandlerPath, (request, response) => {
if (request.session.csrfSecret != request.query.state) {
response.status(400).send("The authorization server returned an invalid state parameter")
}
const indieauthn = new IndieAuthentication({
clientId: opts.clientId,
redirectUri: 'https://' + (process.env.MAIN_URL) + opts.indieAuthHandlerPath,
state: request.query.state,
...request.session.state
})
//console.log(['Verifying ', request.query.code, ' with ', indieauthn.options])
indieauthn.verifyCode(request.query.code)
.then(me => {
request.session.user = {
...request.session.state,
}
delete request.session.state
delete request.session.csrfSecret
var redir = opts.successRedirect;
if( request.session.returnTo ){
redir = request.session.returnTo;
delete request.session.returnTo;
}
response.redirect(redir)
})
.catch(err => {
console.log(err)
response.status(400).send(err)
})
})
return rtr
}

+ 34
- 0
package.json View File

@@ -0,0 +1,34 @@
{
"//1": "describes your app and its dependencies",
"//2": "https://docs.npmjs.com/files/package.json",
"//3": "updating this file will download and update your packages",
"name": "hello-express",
"version": "0.0.1",
"description": "A simple Node app built on Express, instantly up and running.",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.16.3",
"sqlite3": "^4.0.1",
"cookie-session": "^1.3.2",
"express-handlebars": "^3.0.0",
"indieauth-authentication": "^0.0.3",
"hash-emoji-without-borders": "git+https://github.com/martymcguire/hash-emoji-without-borders#master"
},
"engines": {
"node": "8.x"
},
"repository": {
"url": "https://glitch.com/edit/#!/hello-express"
},
"license": "MIT",
"keywords": [
"node",
"glitch",
"express",
"webring",
"indieweb"
]
}

+ 4
- 0
public/client.js View File

@@ -0,0 +1,4 @@
// client-side js
// run by the browser each time your view template is loaded

console.log('hello world :o');

+ 65
- 0
public/images/indiewebcamp-logo-lockup-color.svg View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 487 231.5" style="enable-background:new 0 0 487 231.5;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FF5C00;}
.st1{fill:#FFB100;}
.st2{fill:#666666;}
.st3{fill:#444444;}
.st4{fill:none;}
</style>
<g>
<g>
<g>
<polygon class="st0" points="163.8,33 222.9,159.2 241.5,119.3 201.3,33 "/>
<polygon class="st0" points="332.9,33 282,33 214.9,33 253.7,116.2 273.8,159.2 "/>
</g>
<g>
<g>
<defs>
<rect id="SVGID_1_" x="33" y="33" width="117.8" height="33"/>
</defs>
<use xlink:href="#SVGID_1_" style="overflow:visible;fill:#FF0000;"/>
<clipPath id="SVGID_2_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
</g>
<g>
<defs>
<rect id="SVGID_3_" x="33" y="78" width="117.8" height="71.8"/>
</defs>
<use xlink:href="#SVGID_3_" style="overflow:visible;fill:#FF0000;"/>
<clipPath id="SVGID_4_">
<use xlink:href="#SVGID_3_" style="overflow:visible;"/>
</clipPath>
</g>
</g>
<path class="st1" d="M453.1,97.9c-3.1,32.4-30.3,57.8-63.5,57.8c-35.2,0-63.8-28.6-63.8-63.8S354.3,28,389.6,28
c33.1,0,60.3,25.2,63.5,57.5h-63.5c-3.4,0-6.2,2.8-6.2,6.2c0,3.4,2.8,6.2,6.2,6.2H453.1z"/>
<g>
<path class="st2" d="M37.7,198.5h-4.3v-24.2h4.3V198.5z"/>
<path class="st2" d="M59,174.1l15.3,15v-14.8h4.3v24.3l-0.2,0.1l-15.3-14.9v14.7h-4.3v-24.3L59,174.1z"/>
<path class="st2" d="M96.9,198.5v-24.2h8.5c9.2,0,12,7,12,12.1c0,5.2-2.7,12.1-12,12.1H96.9z M105.4,194.7c6.7,0,7.7-6.2,7.7-8.3
c0-2.1-1-8.3-7.7-8.3h-4.3v16.6H105.4z"/>
<path class="st2" d="M139.9,198.5h-4.3v-24.2h4.3V198.5z"/>
<path class="st2" d="M164.6,188.4v6.2h12v3.8h-16.2v-24.2h16.2v3.8h-12v6.6h9.4v3.8H164.6z"/>
<path class="st3" d="M211.9,183.6l-6.7,15h-0.3l-11.1-24.4h4.7l6.5,15.4l6.6-15.8h0.3l6.7,15.8l6.5-15.4h4.7l-11.1,24.4h-0.3
L211.9,183.6z"/>
<path class="st3" d="M248.7,188.4v6.2h12v3.8h-16.2v-24.2h16.2v3.8h-12v6.6h9.4v3.8H248.7z"/>
<path class="st3" d="M295,186.3c2,0.8,3,3.2,3,5.1c0,2.9-2,7-8.5,7h-9.1v-24.2h9.1c6.5,0,8.5,4.1,8.5,7.1
C298,183.3,297,185.5,295,186.3z M289.9,184.6c3.2-0.1,3.7-2.3,3.7-3.3c0-0.8-0.6-3.3-3.9-3.3h-5.2v6.6H289.9z M289.8,194.7
c3.3,0,3.9-2.4,3.9-3.2c0-1.1-0.6-2.9-3.7-3h-5.4v6.2H289.8z"/>
<path class="st2" d="M334.7,180.8c-1.3-1.6-3.9-3-6.4-3c-6.7,0-7.7,6.4-7.7,8.5s1,8.5,7.7,8.5c2.6,0,5.1-1.5,6.7-3l2.5,3.1
c-2.4,2.4-5.2,3.8-9.1,3.8c-9.3,0-12-7.3-12-12.5c0-5.1,2.8-12.4,12-12.4c4.2,0,6.9,1.7,9.2,4.2L334.7,180.8z"/>
<path class="st2" d="M372.4,198.5h-4.7l-2.6-6.2h-7.9l-2.6,6.2H350l11.1-24.4h0.3L372.4,198.5z M358.7,188.8h5l-2.5-6.1
L358.7,188.8z"/>
<path class="st2" d="M398.6,198.8l-7-14.3l-4.6,14h-4.5L391,174h0.3l7.5,16.5l7.5-16.5h0.3l8.6,24.5h-4.5l-4.6-14l-7,14.3H398.6z
"/>
<path class="st2" d="M434.7,198.5h-4.3v-24.2h9.1c6.5,0,8.5,4.6,8.5,7.7c0,4.7-4.2,7.8-8.1,7.8h-5.4V198.5z M439.8,185.9
c3.2-0.1,4-2.7,4-3.9c0-1.3-0.7-3.9-4.1-3.9h-5.1v7.9H439.8z"/>
</g>
</g>
<rect class="st4" width="487" height="231.5"/>
</g>
</svg>

+ 71
- 0
public/style.css View File

@@ -0,0 +1,71 @@
/* styles */
/* called by your view template */

* {
box-sizing: border-box;
}

body {
font-family: "Benton Sans", "Helvetica Neue", helvetica, arial, sans-serif;
margin: 2em;
}

h1 {
font-style: italic;
color: #373fff;
}

.bold {
font-weight: bold;
}

p {
max-width: 600px;
}

form {
margin-bottom: 25px;
padding: 15px;
background-color: cyan;
display: inline-block;
width: 100%;
max-width: 340px;
border-radius: 3px;
}

input {
display: block;
margin-bottom: 10px;
padding: 5px;
width: 100%;
border: 1px solid lightgrey;
border-radius: 3px;
font-size: 16px;
}

button {
font-size: 16px;
border-radius: 3px;
background-color: lightgrey;
border: 1px solid grey;
box-shadow: 2px 2px teal;
cursor: pointer;
}

button:hover {
background-color: yellow;
}

button:active {
box-shadow: none;
}

li {
margin-bottom: 5px;
}

footer {
margin-top: 50px;
padding-top: 25px;
border-top: 1px solid lightgrey;
}

+ 2
- 0
robots.txt View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /*/

+ 224
- 0
server.js View File

@@ -0,0 +1,224 @@
// server.js
// where your node app starts

// init project
var express = require('express');
var bodyParser = require('body-parser');
var app = express();
app.use(bodyParser.urlencoded({ extended: true }));

// we've started you off with Express,
// but feel free to use whatever libs or frameworks you'd like through `package.json`.

// store session data in user's cookies (so: don't store much of it)
const cookieSession = require('cookie-session')
app.use(cookieSession({
secret: process.env.SECRET, /* for tamper-proofing */
maxAge: 24 * 60 * 60 * 1000 /* 1 day */
}))

// this lib provides express routes to handle the IndieAuth dance
const IndieAuthAuthn = require('./lib/indieauth-authn')
const indieAuth = IndieAuthAuthn({
successRedirect: '/dashboard',
clientId: 'https://' + process.env.MAIN_URL + '/' });
app.use(indieAuth)

// render html with http://handlebarsjs.com/
var exphbs = require('express-handlebars');
var hbs = exphbs.create({
defaultLayout: 'main',
extname: '.hbs',
});
app.engine('hbs', hbs.engine);
app.set('views', __dirname + '/views');
app.set('view engine', 'hbs');

// what if slugs were unicode?
const hashEmoji = require('hash-emoji-without-borders');

// we've gotta parse people's webpages
const jsdom = require("jsdom");
const { JSDOM } = jsdom;

// middleware to always set up a context to pass to templates
app.use(function(req, res, next){
res.locals.context = {};
if(req.session.user) {
res.locals.context['user'] = req.session.user
}
next();
});

// a middleware to kick people to the homepage if they are not logged in
function requireLogin(request, response, next) {
if(! request.session.user) { return response.redirect('/') }
next();
}

// http://expressjs.com/en/starter/static-files.html
app.use(express.static('public'));

// init sqlite db
var fs = require('fs');
var dbFile = './.data/sqlite.db';
var exists = fs.existsSync(dbFile);
var sqlite3 = require('sqlite3').verbose();
var db = new sqlite3.Database(dbFile);

// if ./.data/sqlite.db does not exist, create it
db.serialize(function(){
if (!exists) {
db.run('CREATE TABLE Sites (slug TEXT PRIMARY KEY, url TEXT, active INTEGER, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)');
console.log('New table Sites created!');
db.run('CREATE TABLE SiteChecks (slug TEXT, url TEXT, datetime TEXT DEFAULT CURRENT_TIMESTAMP, result TEXT)');
console.log('New table SiteChecks created!');
}
else {
console.log('Database "Sites" ready to go!');
}
});

function addSite(url) {
return new Promise((fulfill, reject) => {
let slug = hashEmoji(url, 3);
db.run(`INSERT INTO Sites (slug, url, active) VALUES ("${slug}","${url}", 1);`, function(err) {
if(err) {
reject({ message: `Couldn't add site ${url}`, error: err});
} else {
fulfill({url: url, slug: slug, active: 1});
}
})
});
}

function getSite(url, create = false) {
return new Promise((fulfill, reject) => {
db.get(`SELECT * from Sites where url="${url}"`, function(err, row) {
if ((row == undefined) && create) {
addSite(url)
.then((site) => fulfill(site))
.catch((err) => reject(err))
} else {
fulfill(row);
}
});
});
}

function checkSiteLinks(site) {
return new Promise((fulfill, reject) => {
const siteBase = 'https://' + process.env.MAIN_URL;
const expectedPaths = {
next: '/' + site.slug + '/next',
previous: '/' + site.slug + '/previous'
}
JSDOM.fromURL(site.url)
.then(dom => {
const window = dom.window;
const doc = window.document;
let links = doc.querySelectorAll('a');
let candidatesByPath = Array.prototype.map.call(links, (a) => a.href)
.filter(href => href.startsWith(siteBase))
.map(href => new URL(href))
.reduce((by_path, candidate) => {
by_path[decodeURIComponent(candidate.pathname)] = candidate; return by_path;
}, {})
let found = {}
for (let linkType in expectedPaths) {
if (candidatesByPath[expectedPaths[linkType]]) {
found[linkType] = decodeURI(candidatesByPath[expectedPaths[linkType]].href);
delete candidatesByPath[expectedPaths[linkType]];
delete expectedPaths[linkType];
}
}
let result = {
found,
missing: expectedPaths,
mystery: candidatesByPath,
active: (Object.keys(found).length > 0)
};
// remove any empties
for (let key of ['found','missing','mystery']) {
if(Object.keys(result[key]).length === 0) {
delete result[key];
}
}
fulfill(result);
window.close();
})
.catch(err => {
fulfill({ status: 0, error: `Problem checking site link: ${err}` });
})
});
}

function saveSiteCheckStatus(site, status) {
db.run(`INSERT INTO SiteChecks (slug, url, result) VALUES (?, ?, ?)`, site.slug,site.url, JSON.stringify(status));
db.run(`UPDATE Sites SET ACTIVE = ? WHERE SLUG = ?;`, status.active, site.slug);
}

// http://expressjs.com/en/starter/basic-routing.html
app.get("/", function (request, response) {
response.render('index', response.locals.context)
});

app.get('/dashboard', requireLogin, function(request, response) {
getSite(request.session.user.me, true) // find site in the db or add it
.then((site) => {
db.all(`SELECT * FROM SiteChecks WHERE url="${site.url}" ORDER BY datetime DESC LIMIT 3`, function(err, rows) {
let parsed_rows = Array.prototype.map.call(rows, row => {
row.result = JSON.parse(row.result);
return row;
});
response.locals.context['site'] = site;
response.locals.context['hostname'] = process.env.MAIN_URL;
response.locals.context['checks'] = rows;
response.render('dashboard', response.locals.context);
});
});
});

app.post('/check-links', requireLogin, function(request, response) {
getSite(request.session.user.me) // find site in db but don't add if missing
.then((site) => {
if(site) {
checkSiteLinks(site)
.then((status) => {
saveSiteCheckStatus(site, status);
response.redirect('/dashboard');
});
}
});
});

app.get('/:slug/next', function(request, response) {
db.get(`SELECT * from Sites where slug="${request.params.slug}"`, function(err, row) {
db.get(`SELECT * from Sites where active = 1 AND slug != "${request.params.slug}" ORDER BY RANDOM() LIMIT 1`, function(err, row) {
response.redirect(row['url']);
});
});
});

app.get('/:slug/previous', function(request, response) {
db.get(`SELECT * from Sites where slug="${request.params.slug}"`, function(err, row) {
db.get(`SELECT * from Sites where active = 1 AND slug != "${request.params.slug}" ORDER BY RANDOM() LIMIT 1`, function(err, row) {
response.redirect(row['url']);
});
});
});

app.get('/:slug', function(request, response) {
db.get(`SELECT * from Sites where slug="${request.params.slug}"`, function(err, row) {
if(row) {
response.redirect(row['url']);
} else {
response.redirect('/');
}
});
});

// listen for requests :)
var listener = app.listen(process.env.PORT, function () {
console.log('Your app is listening on port ' + listener.address().port);
});

+ 1739
- 0
shrinkwrap.yaml
File diff suppressed because it is too large
View File


+ 27
- 0
views/dashboard.hbs View File

@@ -0,0 +1,27 @@
<h3>
Hello, {{ site.url }} ({{site.slug}})!
</h3>

<div>
Your site is currently: {{#unless site.active }}NOT {{/unless}} ACTIVE.
</div>

<div>
To stay active, make sure links like these are visible on your site:
</div>

<div>
<textarea rows="6" cols="60">
<a href="https://{{ hostname }}/{{site.slug}}/previous">←</a>
An IndieWeb Webring 🕸💍
<a href="https://{{ hostname }}/{{site.slug}}/next">→</a>
</textarea>
</div>

<form action="/check-links" method="POST">
<input type="submit" value="Check links now!"/>
</form>

{{#if checks }}
{{> checks }}
{{/if}}

+ 10
- 0
views/index.hbs View File

@@ -0,0 +1,10 @@
<p>Get started by signing in.</p>

{{> sign-in-form }}

<h3>
Recent Changes
</h3>
<ul>
<li><strong>2018-09-29</strong> – <em>Webrings without borders</em>! New emoji IDs will no longer have country flag emoji. <a href="https://martymcgui.re/2018/09/29/114553/">[Details and comments]</a></li>
</ul>

+ 57
- 0
views/layouts/main.hbs View File

@@ -0,0 +1,57 @@
<!-- This is a static file -->
<!-- served from your routes in server.js -->

<!-- You might want to try something fancier: -->
<!-- html/nunjucks docs: https://mozilla.github.io/nunjucks/ -->
<!-- pug: https://pugjs.org/ -->
<!-- haml: http://haml.info/ -->
<!-- hbs(handlebars): http://handlebarsjs.com/ -->

<!DOCTYPE html>
<html lang="en">
<head>
<title>🕸💍</title>
<meta name="description" content="An IndieWeb Webring">
<link id="favicon" rel="icon" href="https://glitch.com/edit/favicon-app.ico" type="image/x-icon">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">

<!-- import the webpage's stylesheet -->
<link rel="stylesheet" href="/style.css">
</head>
<body>
<header>
<h1>
An <img style="height: 3em; margin-bottom: -1em" src="/images/indiewebcamp-logo-lockup-color.svg" alt="IndieWebCamp logo" /> Web Ring
</h1>
<h2>
Or: if you like it then you should put it on a 🕸💍
</h2>
</header>

<main>
{{{body}}}
</main>

<footer>
<p>
Made with <a href="https://glitch.com">Glitch</a> and ❤️ at <a href="https://indieweb.org/2018">IndieWeb Summit 2018</a> by <a href="https://martymcgui.re/">schmarty</a>.
</p>
<p>
Having trouble? Find schmarty in the <a href="https://indieweb.org/discuss">IndieWeb chat</a>!
</p>
</footer>
<!-- import the webpage's client-side javascript file -->
<!-- <script src="/client.js"></script> -->

<!-- include the Glitch button to show what the webpage is about and
to make it easier for folks to view source and remix
<div class="glitchButton" style="position:fixed;top:20px;right:20px;"></div>
<script src="https://button.glitch.me/button.js"></script>
-->

</body>
</html>

+ 21
- 0
views/partials/checks-result.hbs View File

@@ -0,0 +1,21 @@
{{#if found }}
<ul class="found">
{{#each found }}
<li>{{ @key }} - {{ . }}</li>
{{/each}}
</ul>
{{/if}}
{{#if missing }}
<ul class="missing">
{{#each missing }}
<li>{{ @key }} - {{ . }}</li>
{{/each}}
</ul>
{{/if}}
{{#if mystery }}
<ul class="mystery">
{{#each mystery }}
<li>{{ @key }} - {{ . }}</li>
{{/each}}
</ul>
{{/if}}

+ 39
- 0
views/partials/checks.hbs View File

@@ -0,0 +1,39 @@
<table class="site-checks-table">
<thead>
<tr><th>Active?</th><th>Checked</th><th>Link Summary</th></tr>
</thead>
<tbody>
{{#each checks}}
<tr><td>{{#if result.active }}✓{{else}}X{{/if}}</td><td>{{ datetime }}</td><td class="check-results">{{#with result}}{{> checks-result }}{{/with}}</td></tr>
{{/each}}
</tbody>
</table>
<style>
.site-checks-table td {
vertical-align: baseline;
}
.check-results ul {
list-style: none;
}
.check-results ul li {
padding: 0 1em;
}
.check-results ul.found li {
background-color: hsl(120,50%,90%);
}
.check-results ul.found li:before {
content: '✓ ';
}
.check-results ul.mystery li {
background-color: hsl(52,100%,90%);
}
.check-results ul.mystery li:before {
content: '? ';
}
.check-results ul.missing li {
background-color: hsl(344,66%,90%);
}
.check-results ul.missing li:before {
content: 'X ';
}
</style>

+ 5
- 0
views/partials/sign-in-form.hbs View File

@@ -0,0 +1,5 @@
<form method="POST" action="/signin">
<label for="me">Sign in with your domain:</label>
<input type="url" id="me" name="me"/>
<button type="submit">Sign In</button>
</form>

Loading…
Cancel
Save