Add register, create and my-videos pages with shared app assets
This commit is contained in:
parent
7674aad942
commit
5509c03627
5 changed files with 521 additions and 0 deletions
77
assets/app.css
Normal file
77
assets/app.css
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
:root{
|
||||||
|
--bg:#0e0f13; --panel:#16181f; --line:#262a35;
|
||||||
|
--ink:#e9ecf2; --muted:#9aa3b2; --accent:#4B8BF5; --accent2:#f5c04b;
|
||||||
|
--bad:#f5714b; --good:#4bf58b;
|
||||||
|
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;background:var(--bg);color:var(--ink);
|
||||||
|
font:16px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}
|
||||||
|
.wrap{max-width:940px;margin:0 auto;padding:0 22px}
|
||||||
|
a{color:var(--accent)}
|
||||||
|
|
||||||
|
nav{border-bottom:1px solid var(--line);padding:18px 0;margin-bottom:8px}
|
||||||
|
nav .inner{max-width:940px;margin:0 auto;padding:0 22px;display:flex;gap:22px;align-items:center;flex-wrap:wrap}
|
||||||
|
nav .brand{font-weight:700;letter-spacing:-.01em;color:var(--ink);text-decoration:none}
|
||||||
|
nav .brand span{color:var(--accent)}
|
||||||
|
nav a.link{color:var(--muted);text-decoration:none;font-size:14.5px}
|
||||||
|
nav a.link:hover,nav a.link.on{color:var(--ink)}
|
||||||
|
nav .who{margin-left:auto;font:12.5px/1 var(--mono);color:var(--muted);display:flex;gap:12px;align-items:center}
|
||||||
|
nav .who button{background:none;border:0;color:var(--muted);cursor:pointer;font:inherit;text-decoration:underline}
|
||||||
|
|
||||||
|
header.page{padding:46px 0 30px}
|
||||||
|
header.page h1{font-size:clamp(28px,5vw,42px);line-height:1.08;margin:0 0 12px;letter-spacing:-.02em}
|
||||||
|
header.page p{color:var(--muted);max-width:62ch;margin:0;font-size:17px}
|
||||||
|
section{padding:34px 0}
|
||||||
|
h2.sec{font-size:13px;font-family:var(--mono);letter-spacing:.16em;text-transform:uppercase;
|
||||||
|
color:var(--muted);margin:0 0 20px;font-weight:600}
|
||||||
|
|
||||||
|
.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:26px}
|
||||||
|
.field{margin-bottom:20px}
|
||||||
|
.field label{display:block;font-size:14px;font-weight:600;margin-bottom:6px}
|
||||||
|
.field .hint{display:block;color:var(--muted);font-size:13.5px;margin-bottom:9px;font-weight:400}
|
||||||
|
input[type=text],input[type=email],input[type=url],textarea,select{
|
||||||
|
width:100%;background:#0f1117;border:1px solid var(--line);border-radius:9px;
|
||||||
|
color:var(--ink);padding:11px 13px;font:15px/1.5 inherit}
|
||||||
|
input:focus,textarea:focus,select:focus{outline:none;border-color:var(--accent)}
|
||||||
|
textarea{min-height:110px;resize:vertical}
|
||||||
|
.row{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
||||||
|
@media(max-width:640px){.row{grid-template-columns:1fr}}
|
||||||
|
|
||||||
|
.btn{background:var(--accent);color:#fff;border:0;border-radius:9px;padding:12px 22px;
|
||||||
|
font:600 15px/1 inherit;cursor:pointer}
|
||||||
|
.btn:hover{filter:brightness(1.08)}
|
||||||
|
.btn[disabled]{opacity:.5;cursor:default}
|
||||||
|
.btn.ghost{background:none;border:1px solid var(--line);color:var(--ink)}
|
||||||
|
|
||||||
|
.msg{border-radius:9px;padding:12px 15px;margin-bottom:18px;font-size:14.5px;display:none}
|
||||||
|
.msg.err{display:block;background:#2a1512;border:1px solid #5b2a20;color:#ffb9a7}
|
||||||
|
.msg.ok{display:block;background:#10241a;border:1px solid #1f5138;color:#a7f0c8}
|
||||||
|
|
||||||
|
.chips{display:flex;gap:10px;flex-wrap:wrap}
|
||||||
|
.chip{border:1px solid var(--line);border-radius:999px;padding:8px 15px;cursor:pointer;font-size:14px;color:var(--muted);background:#0f1117}
|
||||||
|
.chip.on{border-color:var(--accent);color:var(--ink);background:#141a28}
|
||||||
|
|
||||||
|
.quote{border:1px solid var(--line);border-radius:10px;padding:15px 18px;background:#0f1117;
|
||||||
|
display:flex;justify-content:space-between;align-items:baseline;gap:14px;flex-wrap:wrap}
|
||||||
|
.quote .big{font-size:26px;font-weight:700;letter-spacing:-.01em}
|
||||||
|
.quote .sub{color:var(--muted);font-size:13.5px;font-family:var(--mono)}
|
||||||
|
|
||||||
|
.list{display:grid;gap:16px}
|
||||||
|
.item{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px;
|
||||||
|
display:grid;grid-template-columns:150px 1fr;gap:22px}
|
||||||
|
@media(max-width:640px){.item{grid-template-columns:1fr}}
|
||||||
|
.item video{width:100%;aspect-ratio:9/16;border-radius:10px;background:#000;border:1px solid var(--line)}
|
||||||
|
.item .ph{width:100%;aspect-ratio:9/16;border-radius:10px;background:#0f1117;border:1px dashed var(--line);
|
||||||
|
display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:12px;text-align:center;padding:12px}
|
||||||
|
.item h3{margin:0 0 6px;font-size:18px}
|
||||||
|
.item .meta{color:var(--muted);font-size:13.5px;margin:0 0 10px;word-break:break-word}
|
||||||
|
.badge{display:inline-block;font:600 11px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;
|
||||||
|
padding:5px 9px;border-radius:5px;margin-bottom:10px}
|
||||||
|
.badge.submitted{background:#1d1a0d;color:var(--accent2);border:1px solid #3a3212}
|
||||||
|
.badge.ready{background:#10241a;color:#7fe3a8;border:1px solid #1f5138}
|
||||||
|
.badge.failed{background:#2a1512;color:#ffb9a7;border:1px solid #5b2a20}
|
||||||
|
|
||||||
|
.empty{border:1px dashed var(--line);border-radius:14px;padding:44px 26px;text-align:center;color:var(--muted)}
|
||||||
|
.note{background:#1a1410;border:1px solid #3d2f1c;border-radius:12px;padding:16px 19px;color:#d9c9ae;font-size:14.5px}
|
||||||
|
footer{padding:44px 0 70px;color:var(--muted);font-size:13.5px;border-top:1px solid var(--line);margin-top:40px}
|
||||||
97
assets/app.js
Normal file
97
assets/app.js
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
// PayToLaugh front end.
|
||||||
|
//
|
||||||
|
// NOTE ON THE TOKEN BELOW: it is public by construction. A static page cannot
|
||||||
|
// keep a secret, so the endpoint behind it is built to be safe in public
|
||||||
|
// hands. It reads and writes small records only and cannot spend money.
|
||||||
|
// Script and video generation are deliberately NOT reachable from here.
|
||||||
|
//
|
||||||
|
// NOTE ON "LOGIN": an email address identifies you, it does not authenticate
|
||||||
|
// you. Anyone who knows an address can see that account. Temporary, by
|
||||||
|
// agreement, until real auth lands.
|
||||||
|
|
||||||
|
var API = "https://api.corben.world/functions/da0474ea-4e56-405c-9f0d-35c3e4037a69/invoke"
|
||||||
|
+ "?token=97cd624cd2bd6a74eb692a940e71851f";
|
||||||
|
|
||||||
|
async function api(action, payload) {
|
||||||
|
var body = Object.assign({ action: action }, payload || {});
|
||||||
|
var res;
|
||||||
|
try {
|
||||||
|
res = await fetch(API, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: "Could not reach the server. Check your connection and try again." };
|
||||||
|
}
|
||||||
|
var outer;
|
||||||
|
try { outer = await res.json(); }
|
||||||
|
catch (e) { return { ok: false, error: "The server sent something unreadable." }; }
|
||||||
|
// The invoke route wraps the function result.
|
||||||
|
return outer && outer.result ? outer.result : outer;
|
||||||
|
}
|
||||||
|
|
||||||
|
var Session = {
|
||||||
|
key: "ptl_email",
|
||||||
|
nameKey: "ptl_name",
|
||||||
|
get: function () { try { return localStorage.getItem(this.key); } catch (e) { return null; } },
|
||||||
|
name: function () { try { return localStorage.getItem(this.nameKey) || ""; } catch (e) { return ""; } },
|
||||||
|
set: function (email, name) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(this.key, email);
|
||||||
|
if (name) localStorage.setItem(this.nameKey, name);
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
clear: function () {
|
||||||
|
try { localStorage.removeItem(this.key); localStorage.removeItem(this.nameKey); } catch (e) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
return String(s == null ? "" : s).replace(/[&<>\"']/g, function (c) {
|
||||||
|
return { "&": "&", "<": "<", ">": ">", '\"': """, "'": "'" }[c];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function navBar(current) {
|
||||||
|
var email = Session.get();
|
||||||
|
var links = [["/", "Home", "home"], ["/create.html", "New video", "create"], ["/videos.html", "My videos", "videos"]];
|
||||||
|
var html = '<nav><div class="inner">'
|
||||||
|
+ '<a class="brand" href="/">Pay<span>To</span>Laugh</a>'
|
||||||
|
+ links.map(function (l) {
|
||||||
|
return '<a class="link' + (l[2] === current ? ' on' : '') + '" href="' + l[0] + '">' + l[1] + '</a>';
|
||||||
|
}).join("")
|
||||||
|
+ '<div class="who">'
|
||||||
|
+ (email
|
||||||
|
? esc(email) + ' <button type="button" id="ptl-signout">Sign out</button>'
|
||||||
|
: '<a class="link" href="/register.html">Sign in</a>')
|
||||||
|
+ '</div></div></nav>';
|
||||||
|
document.body.insertAdjacentHTML("afterbegin", html);
|
||||||
|
var b = document.getElementById("ptl-signout");
|
||||||
|
if (b) b.onclick = function () { Session.clear(); location.href = "/register.html"; };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pages that need an identity send you to sign in rather than failing oddly.
|
||||||
|
function requireSession() {
|
||||||
|
var e = Session.get();
|
||||||
|
if (!e) { location.href = "/register.html?next=" + encodeURIComponent(location.pathname); return null; }
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMsg(id, text, kind) {
|
||||||
|
var el = document.getElementById(id);
|
||||||
|
if (!el) return;
|
||||||
|
el.className = "msg " + (kind || "err");
|
||||||
|
el.textContent = text;
|
||||||
|
}
|
||||||
|
function hideMsg(id) {
|
||||||
|
var el = document.getElementById(id);
|
||||||
|
if (el) el.className = "msg";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors the server's arithmetic exactly: $0.40 per 8s clip plus $0.17 for
|
||||||
|
// script and plate, at a 5x markup. If one changes, change both.
|
||||||
|
function priceFor(segments) {
|
||||||
|
var cost = segments * 0.40 + 0.17;
|
||||||
|
return { cost: cost, price: Math.round(cost * 5 * 100) / 100, seconds: segments * 8 };
|
||||||
|
}
|
||||||
191
create.html
Normal file
191
create.html
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>New video — PayToLaugh</title>
|
||||||
|
<meta name="description" content="Pick your pet, paste a link, tell the director what you think.">
|
||||||
|
<link rel="stylesheet" href="/assets/app.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<header class="page">
|
||||||
|
<h1>Make a new video</h1>
|
||||||
|
<p>Pick the pet, paste whatever you want them to react to, and tell the director what you actually think. The opinion is yours; the delivery is theirs.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="card">
|
||||||
|
<div class="msg" id="msg"></div>
|
||||||
|
<form id="form">
|
||||||
|
|
||||||
|
<h2 class="sec">1 · Who is talking</h2>
|
||||||
|
<div class="field">
|
||||||
|
<label for="pet">Pet</label>
|
||||||
|
<select id="pet"><option value="__new">Add a new pet…</option></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="newpet">
|
||||||
|
<div class="row">
|
||||||
|
<div class="field">
|
||||||
|
<label for="pname">Name</label>
|
||||||
|
<input type="text" id="pname" placeholder="Astro">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="pbreed">Breed</label>
|
||||||
|
<input type="text" id="pbreed" placeholder="Labrador Retriever">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="pappear">What do they look like</label>
|
||||||
|
<span class="hint">Colour and markings matter more than you would think. Naming them plainly is what keeps the likeness.</span>
|
||||||
|
<input type="text" id="pappear" placeholder="a yellow Labrador with a broad head, dark eyes and a black nose">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="pperson">What are they like</label>
|
||||||
|
<span class="hint">This is what makes it sound like your pet and not a generic one.</span>
|
||||||
|
<input type="text" id="pperson" placeholder="convinced he runs the household, suspicious of anything with wheels">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="sec" style="margin-top:34px">2 · What they are reacting to</h2>
|
||||||
|
<div class="field">
|
||||||
|
<label for="url">Link to an article, post or story</label>
|
||||||
|
<span class="hint">Anything with a claim in it. News piece, a thread, a video.</span>
|
||||||
|
<input type="url" id="url" placeholder="https://…">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="notes">Your opinion, and notes for the director</label>
|
||||||
|
<span class="hint">Say what you think and how you want it played. Two sentences is plenty.</span>
|
||||||
|
<textarea id="notes" placeholder="Astro has hated our robot vacuum since day one and barks at it every time it docks. Play it like he is filing a formal grievance with management."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Do they agree or disagree?</label>
|
||||||
|
<div class="chips" id="stance">
|
||||||
|
<span class="chip on" data-v="auto">Let the writer decide</span>
|
||||||
|
<span class="chip" data-v="agree">Agree</span>
|
||||||
|
<span class="chip" data-v="disagree">Disagree</span>
|
||||||
|
<span class="chip" data-v="mixed">A bit of both</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="sec" style="margin-top:34px">3 · How long</h2>
|
||||||
|
<div class="field">
|
||||||
|
<div class="chips" id="len">
|
||||||
|
<span class="chip" data-v="2">16 seconds</span>
|
||||||
|
<span class="chip on" data-v="4">32 seconds</span>
|
||||||
|
<span class="chip" data-v="6">48 seconds</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="quote">
|
||||||
|
<div><div class="big" id="price">$8.85</div><div class="sub" id="plen">32 seconds · 4 shots</div></div>
|
||||||
|
<div class="sub">You approve the script before anything is filmed.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn" id="go" type="submit" style="margin-top:26px">Submit for scripting</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="note" style="margin-top:22px">
|
||||||
|
<strong>What happens next.</strong> Submitting records your request and puts it in the
|
||||||
|
queue. Nothing is charged and nothing is filmed yet. Payment and automatic
|
||||||
|
production get switched on once proper sign-in is in place.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>PayToLaugh · your pet has opinions.</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/assets/app.js"></script>
|
||||||
|
<script>
|
||||||
|
navBar("create");
|
||||||
|
var email = requireSession();
|
||||||
|
|
||||||
|
var stance = "auto", segments = 4;
|
||||||
|
|
||||||
|
function chipGroup(id, cb) {
|
||||||
|
var g = document.getElementById(id);
|
||||||
|
g.addEventListener("click", function (e) {
|
||||||
|
var c = e.target.closest(".chip");
|
||||||
|
if (!c) return;
|
||||||
|
[].forEach.call(g.querySelectorAll(".chip"), function (x) { x.classList.remove("on"); });
|
||||||
|
c.classList.add("on");
|
||||||
|
cb(c.getAttribute("data-v"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
chipGroup("stance", function (v) { stance = v; });
|
||||||
|
chipGroup("len", function (v) { segments = parseInt(v, 10); paint(); });
|
||||||
|
|
||||||
|
function paint() {
|
||||||
|
var p = priceFor(segments);
|
||||||
|
document.getElementById("price").textContent = "$" + p.price.toFixed(2);
|
||||||
|
document.getElementById("plen").textContent = p.seconds + " seconds · " + segments + " shots";
|
||||||
|
}
|
||||||
|
paint();
|
||||||
|
|
||||||
|
var petSel = document.getElementById("pet");
|
||||||
|
var newPet = document.getElementById("newpet");
|
||||||
|
function togglePet() { newPet.style.display = petSel.value === "__new" ? "" : "none"; }
|
||||||
|
petSel.addEventListener("change", togglePet);
|
||||||
|
|
||||||
|
(async function loadPets() {
|
||||||
|
if (!email) return;
|
||||||
|
var r = await api("list_pets", { email: email });
|
||||||
|
if (r.ok && r.pets && r.pets.length) {
|
||||||
|
r.pets.forEach(function (p) {
|
||||||
|
var o = document.createElement("option");
|
||||||
|
o.value = p.id || p.name;
|
||||||
|
o.textContent = p.name + (p.breed ? " · " + p.breed : "");
|
||||||
|
o.dataset.name = p.name;
|
||||||
|
petSel.insertBefore(o, petSel.firstChild);
|
||||||
|
});
|
||||||
|
petSel.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
togglePet();
|
||||||
|
})();
|
||||||
|
|
||||||
|
document.getElementById("form").addEventListener("submit", async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
hideMsg("msg");
|
||||||
|
var btn = document.getElementById("go");
|
||||||
|
var url = document.getElementById("url").value.trim();
|
||||||
|
var notes = document.getElementById("notes").value.trim();
|
||||||
|
|
||||||
|
var petId = "", petName = "";
|
||||||
|
if (petSel.value === "__new") {
|
||||||
|
petName = document.getElementById("pname").value.trim();
|
||||||
|
if (!petName) { showMsg("msg", "Your pet needs a name."); return; }
|
||||||
|
} else {
|
||||||
|
petId = petSel.value;
|
||||||
|
petName = petSel.options[petSel.selectedIndex].dataset.name || "";
|
||||||
|
}
|
||||||
|
if (!url) { showMsg("msg", "Paste a link to whatever you want them to react to."); return; }
|
||||||
|
if (notes.length < 10) { showMsg("msg", "Tell the director what you think, in a sentence or two."); return; }
|
||||||
|
|
||||||
|
btn.disabled = true; btn.textContent = "Submitting…";
|
||||||
|
|
||||||
|
if (petSel.value === "__new") {
|
||||||
|
var pr = await api("save_pet", { email: email, pet: {
|
||||||
|
name: petName,
|
||||||
|
breed: document.getElementById("pbreed").value.trim(),
|
||||||
|
appearance: document.getElementById("pappear").value.trim(),
|
||||||
|
personality: document.getElementById("pperson").value.trim(),
|
||||||
|
species: "dog"
|
||||||
|
}});
|
||||||
|
if (!pr.ok) { btn.disabled = false; btn.textContent = "Submit for scripting"; showMsg("msg", pr.error || "Could not save that pet."); return; }
|
||||||
|
petId = pr.pet.id || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
var r = await api("create_project", {
|
||||||
|
email: email, pet_id: petId, pet_name: petName,
|
||||||
|
source_url: url, owner_notes: notes, stance: stance, segments: segments
|
||||||
|
});
|
||||||
|
btn.disabled = false; btn.textContent = "Submit for scripting";
|
||||||
|
if (!r.ok) { showMsg("msg", r.error || "That did not go through. Please try again."); return; }
|
||||||
|
showMsg("msg", "Submitted. Taking you to your videos…", "ok");
|
||||||
|
setTimeout(function () { location.href = "/videos.html"; }, 650);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
70
register.html
Normal file
70
register.html
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Sign in — PayToLaugh</title>
|
||||||
|
<meta name="description" content="Create a PayToLaugh account and start making videos of your pet weighing in.">
|
||||||
|
<link rel="stylesheet" href="/assets/app.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<header class="page">
|
||||||
|
<h1>Sign in</h1>
|
||||||
|
<p>Your email is all we need for now. If you have not been here before, this creates your account.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section style="max-width:520px">
|
||||||
|
<div class="card">
|
||||||
|
<div class="msg" id="msg"></div>
|
||||||
|
<form id="form">
|
||||||
|
<div class="field">
|
||||||
|
<label for="email">Email address</label>
|
||||||
|
<span class="hint">We use this to keep your pets and videos together.</span>
|
||||||
|
<input type="email" id="email" name="email" required autocomplete="email" placeholder="you@example.com">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="name">Your name <span style="color:var(--muted);font-weight:400">(optional)</span></label>
|
||||||
|
<input type="text" id="name" name="name" autocomplete="name" placeholder="Scooter">
|
||||||
|
</div>
|
||||||
|
<button class="btn" id="go" type="submit">Continue</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="note" style="margin-top:22px">
|
||||||
|
<strong>Early access.</strong> There is no password yet, so an email address identifies
|
||||||
|
you rather than proves who you are. Do not put anything here you would mind
|
||||||
|
another person seeing. Proper sign-in is coming.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>PayToLaugh · your pet has opinions.</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/assets/app.js"></script>
|
||||||
|
<script>
|
||||||
|
navBar("");
|
||||||
|
var params = new URLSearchParams(location.search);
|
||||||
|
var next = params.get("next") || "/create.html";
|
||||||
|
|
||||||
|
document.getElementById("form").addEventListener("submit", async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
hideMsg("msg");
|
||||||
|
var btn = document.getElementById("go");
|
||||||
|
var email = document.getElementById("email").value.trim();
|
||||||
|
var name = document.getElementById("name").value.trim();
|
||||||
|
if (!email) { showMsg("msg", "Please enter your email address."); return; }
|
||||||
|
|
||||||
|
btn.disabled = true; btn.textContent = "One moment…";
|
||||||
|
var r = await api("register", { email: email, display_name: name });
|
||||||
|
btn.disabled = false; btn.textContent = "Continue";
|
||||||
|
|
||||||
|
if (!r.ok) { showMsg("msg", r.error || "That did not work. Please try again."); return; }
|
||||||
|
Session.set(r.user.email, r.user.display_name || name);
|
||||||
|
showMsg("msg", r.returning ? "Welcome back. Taking you through…" : "Account created. Taking you through…", "ok");
|
||||||
|
setTimeout(function () { location.href = next; }, 550);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
86
videos.html
Normal file
86
videos.html
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>My videos — PayToLaugh</title>
|
||||||
|
<meta name="description" content="Everything you have submitted, and every video that has come back.">
|
||||||
|
<link rel="stylesheet" href="/assets/app.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<header class="page">
|
||||||
|
<h1>My videos</h1>
|
||||||
|
<p>Everything you have submitted, newest first.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="msg" id="msg"></div>
|
||||||
|
<div id="out"><p style="color:var(--muted)">Loading…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>PayToLaugh · your pet has opinions.</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/assets/app.js"></script>
|
||||||
|
<script>
|
||||||
|
navBar("videos");
|
||||||
|
var email = requireSession();
|
||||||
|
|
||||||
|
var LABEL = {
|
||||||
|
submitted: ["submitted", "In the queue"],
|
||||||
|
scripting: ["submitted", "Being written"],
|
||||||
|
script_ready: ["submitted", "Script ready for you"],
|
||||||
|
rendering: ["submitted", "Filming"],
|
||||||
|
ready: ["ready", "Ready"],
|
||||||
|
failed: ["failed", "Did not work"]
|
||||||
|
};
|
||||||
|
|
||||||
|
function when(s) {
|
||||||
|
if (!s) return "";
|
||||||
|
var d = new Date(s);
|
||||||
|
return isNaN(d) ? s : d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function card(p) {
|
||||||
|
var st = LABEL[p.status] || ["submitted", p.status || "In the queue"];
|
||||||
|
var media = p.video_url
|
||||||
|
? '<video controls playsinline preload="metadata" src="' + esc(p.video_url) + '"></video>'
|
||||||
|
: '<div class="ph">Not filmed yet</div>';
|
||||||
|
var dl = p.video_url
|
||||||
|
? '<a class="btn ghost" style="display:inline-block;text-decoration:none;margin-top:10px" href="'
|
||||||
|
+ esc(p.video_url) + '" download>Download</a>'
|
||||||
|
: "";
|
||||||
|
return '<div class="item">'
|
||||||
|
+ '<div>' + media + '</div>'
|
||||||
|
+ '<div>'
|
||||||
|
+ '<span class="badge ' + st[0] + '">' + esc(st[1]) + '</span>'
|
||||||
|
+ '<h3>' + esc(p.pet_name || "Your pet") + ' reacts</h3>'
|
||||||
|
+ '<p class="meta">' + (p.source_url ? '<a href="' + esc(p.source_url) + '" rel="noopener noreferrer nofollow" target="_blank">' + esc(p.source_url) + '</a>' : "") + '</p>'
|
||||||
|
+ '<p class="meta">' + esc(p.owner_notes || "") + '</p>'
|
||||||
|
+ '<p class="meta">' + (p.segments || 4) * 8 + ' seconds · '
|
||||||
|
+ (p.price_usd != null ? "$" + Number(p.price_usd).toFixed(2) : "") + ' · ' + esc(when(p.created_at)) + '</p>'
|
||||||
|
+ dl
|
||||||
|
+ '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function () {
|
||||||
|
if (!email) return;
|
||||||
|
var r = await api("list_projects", { email: email });
|
||||||
|
var out = document.getElementById("out");
|
||||||
|
if (!r.ok) {
|
||||||
|
out.innerHTML = "";
|
||||||
|
showMsg("msg", r.error || "Could not load your videos.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!r.projects || !r.projects.length) {
|
||||||
|
out.innerHTML = '<div class="empty"><p style="margin:0 0 16px">Nothing here yet.</p>'
|
||||||
|
+ '<a class="btn" style="display:inline-block;text-decoration:none" href="/create.html">Make your first video</a></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out.innerHTML = '<div class="list">' + r.projects.map(card).join("") + '</div>';
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in a new issue