diff --git a/assets/app.js b/assets/app.js index 711f6a0..52c54cd 100644 --- a/assets/app.js +++ b/assets/app.js @@ -49,10 +49,58 @@ var Session = { } }; +// ONE WEBSITE, SEVERAL FRONT DOORS +// The hostname the page was loaded from decides the look (a stylesheet), the +// name in the corner, the menu and which pages exist. The settings come from +// the server (`site_config`), keyed on the hostname sent with every call, and +// are cached so the look is right on the first paint of the next page. +// `?site=book` on any address previews another front door for this tab. +// This is presentation, not permission: what an account may do is decided by +// its session on the server. +var Site = { + told: (function () { + var pick = null; + try { + pick = new URLSearchParams(location.search).get("site"); + if (pick) sessionStorage.setItem("ptl_site_pick", pick); + else pick = sessionStorage.getItem("ptl_site_pick"); + } catch (e) {} + return (pick || location.hostname || "").toLowerCase(); + })(), + key: function () { return "ptl_site:" + this.told; }, + get: function () { + try { return JSON.parse(localStorage.getItem(this.key()) || "null"); } catch (e) { return null; } + }, + set: function (cfg) { + if (!cfg) return; + try { localStorage.setItem(this.key(), JSON.stringify(cfg)); } catch (e) {} + this.apply(cfg); + }, + apply: function (cfg) { + cfg = cfg || this.get(); + if (!cfg) return; + document.documentElement.setAttribute("data-site", cfg.key || ""); + var want = cfg.theme ? "/assets/site-" + cfg.theme + ".css" : ""; + var link = document.getElementById("ptl-site-css"); + if (want && !link) { + link = document.createElement("link"); + link.id = "ptl-site-css"; link.rel = "stylesheet"; link.href = want; + document.head.appendChild(link); + } else if (link && link.getAttribute("href") !== want) { + if (want) link.href = want; else link.remove(); + } + }, + // Where somebody with no session is sent: the texted-code page once that is + // the rule, the email-only page until then. + signIn: function () { return ((this.get() || {}).auth === "sms") ? "/login.html" : "/register.html"; } +}; +Site.apply(); + async function api(action, payload) { var body = Object.assign({ action: action }, payload || {}); var tok = Session.token(); if (tok) body.session = tok; + body.site = Site.told; var res; try { res = await fetch(API, { @@ -72,10 +120,11 @@ async function api(action, payload) { // failing oddly on every call afterwards. if (r && r.signed_out) { Session.clear(); - if (!/register\.html/.test(location.pathname)) { - location.href = "/register.html?next=" + encodeURIComponent(location.pathname); + if (!/(register|login|join|demo)\.html/.test(location.pathname)) { + location.href = Site.signIn() + "?next=" + encodeURIComponent(location.pathname + location.search); } } + if (r && r.site) Site.set(r.site); return r; } @@ -93,8 +142,13 @@ function navBar(current) { function paint(list) { var old = document.querySelector("nav"); + var site = Site.get() || { brand: ["Pay", "To", "Laugh"], home: "/" }; + var b3 = site.brand || ["Pay", "To", "Laugh"]; + // A page that does not exist at this front door sends the visitor home. + if (current && site.pages && site.pages.indexOf(current) < 0 && site.home + && location.pathname !== site.home) { location.replace(site.home); return; } var html = ''; if (old) old.outerHTML = html; else document.body.insertAdjacentHTML("afterbegin", html); @@ -110,29 +164,36 @@ function navBar(current) { if (b) b.onclick = async function () { await api("sign_out", {}); Session.clear(); - location.href = "/register.html"; + location.href = Site.signIn(); }; } paint(items); - if (Session.token()) { - api("whoami", {}).then(function (r) { - if (r && r.ok && r.menu) { - try { localStorage.setItem(Session.menuKey, JSON.stringify(r.menu)); } catch (e) {} - paint(r.menu); - } - }); - } + // The server says what this visitor's menu is - signed in or not - and what + // this front door looks like. api() stores the site settings as they arrive. + api(Session.token() ? "whoami" : "site_config", {}).then(function (r) { + if (r && r.ok && r.menu) { + try { localStorage.setItem(Session.menuKey, JSON.stringify(r.menu)); } catch (e) {} + paint(r.menu); + } + }); } function requireSession() { if (!Session.token()) { - location.href = "/register.html?next=" + encodeURIComponent(location.pathname); + location.href = Site.signIn() + "?next=" + encodeURIComponent(location.pathname + location.search); return null; } return Session.get(); } +// Only ever a path on this site: a `next` from the address bar is not trusted +// to be one. +function safeNext(next, fallback) { + return (typeof next === "string" && /^\/[A-Za-z0-9_\-\/.]*(\?[^#]*)?$/.test(next) && next.indexOf("//") < 0) + ? next : (fallback || "/"); +} + function showMsg(id, text, kind) { var el = document.getElementById(id); if (!el) return; diff --git a/assets/site-book.css b/assets/site-book.css new file mode 100644 index 0000000..c9467b7 --- /dev/null +++ b/assets/site-book.css @@ -0,0 +1,28 @@ +/* The "Book to Film" front door: the same pages, a different look. + Loaded on top of app.css when the site settings name the theme "book" + (assets/app.js, Site.apply). Everything in app.css is drawn from these + variables, so a look is mostly a palette and a typeface. */ +:root{ + --bg:#f6f1e7; --panel:#fffdf8; --line:#ddd3c0; + --ink:#231f1a; --muted:#6f6657; --accent:#8a2f1c; --accent2:#a8761b; + --bad:#b4531f; --good:#2f7d4f; +} +body{font-family:Georgia,"Iowan Old Style","Times New Roman",serif;font-size:17px} +nav{background:#231f1a;border-bottom:0;margin-bottom:0} +nav .brand{color:#f6f1e7;font-family:Georgia,serif;letter-spacing:.01em} +nav .brand span{color:#e0a458} +nav a.link{color:#c9bfae} +nav a.link:hover,nav a.link.on{color:#fff} +nav .who,nav .who button{color:#c9bfae} +header.page h1{font-family:Georgia,serif;font-weight:600;letter-spacing:0} +h2.sec{color:var(--accent)} +input[type=text],input[type=email],input[type=url],textarea,select{background:#fff;color:var(--ink)} +.btn{background:var(--accent);border-radius:4px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif} +.btn.ghost{color:var(--ink)} +.card,.item,.ex{border-radius:6px;box-shadow:0 1px 0 rgba(35,31,26,.06)} +.msg.err{background:#fbe9e2;border-color:#e6b9a8;color:#7a2c14} +.msg.ok{background:#e6f3ea;border-color:#b5d8c1;color:#1f5a38} +.chip,.quote{background:#fff} +.chip.on{background:#f3e7df} +.item .ph{background:#fff} +footer{color:var(--muted)} diff --git a/demo.html b/demo.html new file mode 100644 index 0000000..239e79a --- /dev/null +++ b/demo.html @@ -0,0 +1,84 @@ + + + + + +Examples + + + + + +
+ +
+

See what it makes

+

Each of these was written, voiced and filmed here. The script is beside the video it became.

+
+ +
+
+

Loading the examples…

+
+ + + + +
+ + + + + diff --git a/index.html b/index.html index ec6d08e..7ed0949 100644 --- a/index.html +++ b/index.html @@ -9,6 +9,15 @@ try { var __m = location.pathname.match(/^\/v\/([0-9a-fA-F]{6,12})\/?$/); if (__m) location.replace("/v/index.html?c=" + __m[1] + location.hash); } catch (e) {} +/* Each front door has its own first page (assets/app.js, Site). When the + settings remembered for this hostname name a different one, go there before + anything is painted; a first-ever visit is sent on by the script at the foot + of this page once the settings arrive. */ +try { + var __s = JSON.parse(localStorage.getItem("ptl_site:" + location.hostname.toLowerCase()) || "null"); + if (__s && __s.home && __s.home !== "/" && location.pathname === "/" + && !/[?&]site=/.test(location.search)) location.replace(__s.home); +} catch (e) {} @@ -216,5 +225,12 @@ try { + + diff --git a/join.html b/join.html new file mode 100644 index 0000000..109abe5 --- /dev/null +++ b/join.html @@ -0,0 +1,103 @@ + + + + + +Join the waiting list + + + + +
+ +
+

Join the waiting list

+

We are letting people in a few at a time. Leave your details and we will email you an + invitation when your place comes up. See what people have made.

+
+ +
+
+
+
+
+
+ + +
+
+ + +
+
+
+ + Your invitation is sent here, and it is how you sign in. + +
+
+ + When you sign in we text a short code to this number. It has to be a US cell phone that can receive texts. + +
+ + + +
+
+ + + +

+ We use your phone number only to text sign-in codes, and your email only for your + invitation and your account. Message and data rates may apply to the texts. +

+
+ + +
+ + + + + diff --git a/login.html b/login.html new file mode 100644 index 0000000..6a57b9b --- /dev/null +++ b/login.html @@ -0,0 +1,129 @@ + + + + + +Sign in + + + + + +
+ +
+

Sign in

+

Enter your email address and we will text a code to the phone on your account.

+
+ +
+
+
+ + + + + + +
+ + +
+
+ + + + + diff --git a/people.html b/people.html new file mode 100644 index 0000000..396b7b4 --- /dev/null +++ b/people.html @@ -0,0 +1,160 @@ + + + + + +People + + + + + +
+ +
+

People

+

Who is waiting, who has been invited, who is in — and which finished videos are shown on the Examples page.

+
+ +
+ +
+

How people get in

+
Loading…
+

+
+ +
+

The waiting list

+
+
+ +
+

Accounts

+
+
+ +
+

Examples page

+
Loading…
+
+
+ + + + + diff --git a/register.html b/register.html index 16bbee3..d139385 100644 --- a/register.html +++ b/register.html @@ -46,7 +46,9 @@