Split pet profiles onto their own page with typed photo uploads
This commit is contained in:
parent
6439a89daa
commit
8202feb299
3 changed files with 275 additions and 64 deletions
|
|
@ -12,6 +12,11 @@
|
||||||
var API = "https://api.corben.world/functions/da0474ea-4e56-405c-9f0d-35c3e4037a69/invoke"
|
var API = "https://api.corben.world/functions/da0474ea-4e56-405c-9f0d-35c3e4037a69/invoke"
|
||||||
+ "?token=97cd624cd2bd6a74eb692a940e71851f";
|
+ "?token=97cd624cd2bd6a74eb692a940e71851f";
|
||||||
|
|
||||||
|
var PHOTO_KINDS = ["head shot", "profile", "full body", "sitting",
|
||||||
|
"playing", "sleeping", "action", "other"];
|
||||||
|
// A plate is built from a head-on shot; these are the ones worth having.
|
||||||
|
var REFERENCE_KINDS = ["head shot", "profile", "full body"];
|
||||||
|
|
||||||
async function api(action, payload) {
|
async function api(action, payload) {
|
||||||
var body = Object.assign({ action: action }, payload || {});
|
var body = Object.assign({ action: action }, payload || {});
|
||||||
var res;
|
var res;
|
||||||
|
|
@ -27,7 +32,6 @@ async function api(action, payload) {
|
||||||
var outer;
|
var outer;
|
||||||
try { outer = await res.json(); }
|
try { outer = await res.json(); }
|
||||||
catch (e) { return { ok: false, error: "The server sent something unreadable." }; }
|
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;
|
return outer && outer.result ? outer.result : outer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,7 +59,8 @@ function esc(s) {
|
||||||
|
|
||||||
function navBar(current) {
|
function navBar(current) {
|
||||||
var email = Session.get();
|
var email = Session.get();
|
||||||
var links = [["/", "Home", "home"], ["/create.html", "New video", "create"], ["/videos.html", "My videos", "videos"]];
|
var links = [["/", "Home", "home"], ["/pets.html", "My pets", "pets"],
|
||||||
|
["/create.html", "New video", "create"], ["/videos.html", "My videos", "videos"]];
|
||||||
var html = '<nav><div class="inner">'
|
var html = '<nav><div class="inner">'
|
||||||
+ '<a class="brand" href="/">Pay<span>To</span>Laugh</a>'
|
+ '<a class="brand" href="/">Pay<span>To</span>Laugh</a>'
|
||||||
+ links.map(function (l) {
|
+ links.map(function (l) {
|
||||||
|
|
@ -71,7 +76,6 @@ function navBar(current) {
|
||||||
if (b) b.onclick = function () { Session.clear(); location.href = "/register.html"; };
|
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() {
|
function requireSession() {
|
||||||
var e = Session.get();
|
var e = Session.get();
|
||||||
if (!e) { location.href = "/register.html?next=" + encodeURIComponent(location.pathname); return null; }
|
if (!e) { location.href = "/register.html?next=" + encodeURIComponent(location.pathname); return null; }
|
||||||
|
|
@ -95,3 +99,37 @@ function priceFor(segments) {
|
||||||
var cost = segments * 0.40 + 0.17;
|
var cost = segments * 0.40 + 0.17;
|
||||||
return { cost: cost, price: Math.round(cost * 5 * 100) / 100, seconds: segments * 8 };
|
return { cost: cost, price: Math.round(cost * 5 * 100) / 100, seconds: segments * 8 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shrink in the browser before upload. Phone photos run to 5-10MB, which is
|
||||||
|
// slow to send and far more detail than a reference plate needs; 1600px is
|
||||||
|
// ample. Returns base64 without the data: prefix.
|
||||||
|
function resizeImage(file, maxPx, quality) {
|
||||||
|
maxPx = maxPx || 1600;
|
||||||
|
quality = quality || 0.85;
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onerror = function () { reject(new Error("Could not read that file.")); };
|
||||||
|
reader.onload = function () {
|
||||||
|
var img = new Image();
|
||||||
|
img.onerror = function () { reject(new Error("That file is not an image we can read.")); };
|
||||||
|
img.onload = function () {
|
||||||
|
var w = img.naturalWidth, h = img.naturalHeight;
|
||||||
|
if (!w || !h) { reject(new Error("That image looks empty.")); return; }
|
||||||
|
var scale = Math.min(1, maxPx / Math.max(w, h));
|
||||||
|
var cw = Math.round(w * scale), ch = Math.round(h * scale);
|
||||||
|
var c = document.createElement("canvas");
|
||||||
|
c.width = cw; c.height = ch;
|
||||||
|
var g = c.getContext("2d");
|
||||||
|
g.fillStyle = "#ffffff";
|
||||||
|
g.fillRect(0, 0, cw, ch); // flatten transparency for JPEG
|
||||||
|
g.drawImage(img, 0, 0, cw, ch);
|
||||||
|
var url;
|
||||||
|
try { url = c.toDataURL("image/jpeg", quality); }
|
||||||
|
catch (e) { reject(new Error("That image could not be processed.")); return; }
|
||||||
|
resolve({ b64: url.split(",")[1], width: cw, height: ch });
|
||||||
|
};
|
||||||
|
img.src = reader.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
81
create.html
81
create.html
|
|
@ -23,30 +23,8 @@
|
||||||
<h2 class="sec">1 · Who is talking</h2>
|
<h2 class="sec">1 · Who is talking</h2>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="pet">Pet</label>
|
<label for="pet">Pet</label>
|
||||||
<select id="pet"><option value="__new">Add a new pet…</option></select>
|
<span class="hint">Profiles and photos live on <a href="/pets.html">My pets</a>.</span>
|
||||||
</div>
|
<select id="pet"></select>
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<h2 class="sec" style="margin-top:34px">2 · What they are reacting to</h2>
|
<h2 class="sec" style="margin-top:34px">2 · What they are reacting to</h2>
|
||||||
|
|
@ -101,8 +79,7 @@
|
||||||
<script>
|
<script>
|
||||||
navBar("create");
|
navBar("create");
|
||||||
var email = requireSession();
|
var email = requireSession();
|
||||||
|
var stance = "auto", segments = 4, havePets = false;
|
||||||
var stance = "auto", segments = 4;
|
|
||||||
|
|
||||||
function chipGroup(id, cb) {
|
function chipGroup(id, cb) {
|
||||||
var g = document.getElementById(id);
|
var g = document.getElementById(id);
|
||||||
|
|
@ -125,60 +102,42 @@ function paint() {
|
||||||
paint();
|
paint();
|
||||||
|
|
||||||
var petSel = document.getElementById("pet");
|
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() {
|
(async function loadPets() {
|
||||||
if (!email) return;
|
if (!email) return;
|
||||||
var r = await api("list_pets", { email: email });
|
var r = await api("list_pets", { email: email });
|
||||||
if (r.ok && r.pets && r.pets.length) {
|
if (!r.ok) { showMsg("msg", r.error || "Could not load your pets."); return; }
|
||||||
r.pets.forEach(function (p) {
|
var pets = r.pets || [];
|
||||||
var o = document.createElement("option");
|
if (!pets.length) {
|
||||||
o.value = p.id || p.name;
|
petSel.innerHTML = '<option value="">No pets yet</option>';
|
||||||
o.textContent = p.name + (p.breed ? " · " + p.breed : "");
|
petSel.disabled = true;
|
||||||
o.dataset.name = p.name;
|
document.getElementById("go").disabled = true;
|
||||||
petSel.insertBefore(o, petSel.firstChild);
|
showMsg("msg", "Add a pet first — it only takes a moment on the My pets page.");
|
||||||
});
|
return;
|
||||||
petSel.selectedIndex = 0;
|
|
||||||
}
|
}
|
||||||
togglePet();
|
havePets = true;
|
||||||
|
petSel.innerHTML = pets.map(function (p) {
|
||||||
|
return '<option value="' + esc(p.id) + '" data-name="' + esc(p.name) + '">'
|
||||||
|
+ esc(p.name) + (p.breed ? " · " + esc(p.breed) : "") + '</option>';
|
||||||
|
}).join("");
|
||||||
})();
|
})();
|
||||||
|
|
||||||
document.getElementById("form").addEventListener("submit", async function (e) {
|
document.getElementById("form").addEventListener("submit", async function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
hideMsg("msg");
|
hideMsg("msg");
|
||||||
|
if (!havePets) { showMsg("msg", "Add a pet first on the My pets page."); return; }
|
||||||
var btn = document.getElementById("go");
|
var btn = document.getElementById("go");
|
||||||
var url = document.getElementById("url").value.trim();
|
var url = document.getElementById("url").value.trim();
|
||||||
var notes = document.getElementById("notes").value.trim();
|
var notes = document.getElementById("notes").value.trim();
|
||||||
|
var opt = petSel.options[petSel.selectedIndex];
|
||||||
|
|
||||||
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 (!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; }
|
if (notes.length < 10) { showMsg("msg", "Tell the director what you think, in a sentence or two."); return; }
|
||||||
|
|
||||||
btn.disabled = true; btn.textContent = "Submitting…";
|
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", {
|
var r = await api("create_project", {
|
||||||
email: email, pet_id: petId, pet_name: petName,
|
email: email, pet_id: petSel.value,
|
||||||
|
pet_name: opt ? opt.getAttribute("data-name") : "",
|
||||||
source_url: url, owner_notes: notes, stance: stance, segments: segments
|
source_url: url, owner_notes: notes, stance: stance, segments: segments
|
||||||
});
|
});
|
||||||
btn.disabled = false; btn.textContent = "Submit for scripting";
|
btn.disabled = false; btn.textContent = "Submit for scripting";
|
||||||
|
|
|
||||||
214
pets.html
Normal file
214
pets.html
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>My pets — PayToLaugh</title>
|
||||||
|
<meta name="description" content="Set up your pet's profile and photos once, then reuse it for every video.">
|
||||||
|
<link rel="stylesheet" href="/assets/app.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<header class="page">
|
||||||
|
<h1>My pets</h1>
|
||||||
|
<p>Set a pet up once and reuse them for every video. The description does more work than you would expect — naming the coat and markings plainly is what keeps the likeness.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="msg" id="msg"></div>
|
||||||
|
|
||||||
|
<div class="card" id="formcard">
|
||||||
|
<h2 class="sec" id="formtitle">Add a pet</h2>
|
||||||
|
<form id="petform">
|
||||||
|
<input type="hidden" id="petid" value="">
|
||||||
|
<div class="row">
|
||||||
|
<div class="field">
|
||||||
|
<label for="pname">Name</label>
|
||||||
|
<input type="text" id="pname" placeholder="Astro" required>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="pspecies">Species</label>
|
||||||
|
<select id="pspecies">
|
||||||
|
<option value="dog">Dog</option>
|
||||||
|
<option value="cat">Cat</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="pbreed">Breed</label>
|
||||||
|
<input type="text" id="pbreed" placeholder="American Bulldog">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="pappear">What do they look like</label>
|
||||||
|
<span class="hint">Coat colour and markings, in plain words. This matters more than photo resolution does.</span>
|
||||||
|
<input type="text" id="pappear" placeholder="a white American Bulldog, short coat, broad head with a wide muzzle, dark eyes, 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 rather than a generic one.</span>
|
||||||
|
<input type="text" id="pperson" placeholder="convinced he runs the household, suspicious of anything with wheels">
|
||||||
|
</div>
|
||||||
|
<button class="btn" id="save" type="submit">Save pet</button>
|
||||||
|
<button class="btn ghost" id="cancel" type="button" style="display:none;margin-left:8px">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="list" style="margin-top:34px"><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("pets");
|
||||||
|
var email = requireSession();
|
||||||
|
var pets = [], photos = {};
|
||||||
|
|
||||||
|
function kindOptions(sel) {
|
||||||
|
return PHOTO_KINDS.map(function (k) {
|
||||||
|
return '<option value="' + esc(k) + '"' + (k === sel ? ' selected' : '') + '>'
|
||||||
|
+ esc(k.charAt(0).toUpperCase() + k.slice(1)) + '</option>';
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function petCard(p) {
|
||||||
|
var shots = (photos[p.id] || []);
|
||||||
|
var strip = shots.length
|
||||||
|
? shots.map(function (s) {
|
||||||
|
return '<figure style="margin:0">'
|
||||||
|
+ '<img src="' + esc(s.url) + '" alt="' + esc(p.name + " " + s.kind) + '" '
|
||||||
|
+ 'style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:9px;border:1px solid var(--line);display:block">'
|
||||||
|
+ '<figcaption style="font:11px/1.5 var(--mono);color:var(--muted);margin-top:5px;display:flex;justify-content:space-between;gap:6px">'
|
||||||
|
+ '<span>' + esc(s.kind) + '</span>'
|
||||||
|
+ '<button type="button" data-del="' + esc(s.id) + '" style="background:none;border:0;color:var(--muted);cursor:pointer;text-decoration:underline;font:inherit">remove</button>'
|
||||||
|
+ '</figcaption></figure>';
|
||||||
|
}).join("")
|
||||||
|
: '<p style="color:var(--muted);font-size:14px;margin:0;grid-column:1/-1">No photos yet. A head shot is the most useful one to start with.</p>';
|
||||||
|
|
||||||
|
return '<div class="card" style="margin-bottom:20px">'
|
||||||
|
+ '<div style="display:flex;justify-content:space-between;gap:16px;align-items:baseline;flex-wrap:wrap">'
|
||||||
|
+ '<div><h3 style="margin:0 0 4px;font-size:20px">' + esc(p.name) + '</h3>'
|
||||||
|
+ '<p style="margin:0;color:var(--muted);font-size:14px">' + esc([p.breed, p.species].filter(Boolean).join(" · ")) + '</p></div>'
|
||||||
|
+ '<button type="button" class="btn ghost" data-edit="' + esc(p.id) + '">Edit</button>'
|
||||||
|
+ '</div>'
|
||||||
|
+ (p.appearance ? '<p style="color:var(--muted);font-size:14.5px;margin:14px 0 0">' + esc(p.appearance) + '</p>' : '')
|
||||||
|
+ (p.personality ? '<p style="color:var(--muted);font-size:14.5px;margin:8px 0 0">' + esc(p.personality) + '</p>' : '')
|
||||||
|
+ '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:12px;margin-top:20px">' + strip + '</div>'
|
||||||
|
+ '<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:18px;padding-top:18px;border-top:1px solid var(--line)">'
|
||||||
|
+ '<select data-kind="' + esc(p.id) + '" style="width:auto">' + kindOptions("head shot") + '</select>'
|
||||||
|
+ '<input type="file" accept="image/*" data-file="' + esc(p.id) + '" style="width:auto;font-size:14px">'
|
||||||
|
+ '<span data-status="' + esc(p.id) + '" style="font:12.5px/1 var(--mono);color:var(--muted)"></span>'
|
||||||
|
+ '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
var r = await api("list_pets", { email: email });
|
||||||
|
if (!r.ok) { showMsg("msg", r.error || "Could not load your pets."); return; }
|
||||||
|
pets = r.pets || [];
|
||||||
|
var ph = await api("list_photos", { email: email });
|
||||||
|
photos = {};
|
||||||
|
if (ph.ok) (ph.photos || []).forEach(function (s) {
|
||||||
|
(photos[s.pet_id] = photos[s.pet_id] || []).push(s);
|
||||||
|
});
|
||||||
|
|
||||||
|
var el = document.getElementById("list");
|
||||||
|
if (!pets.length) {
|
||||||
|
el.innerHTML = '<div class="empty">No pets yet. Add one above and they are ready for every video.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.innerHTML = pets.map(petCard).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("petform").addEventListener("submit", async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
hideMsg("msg");
|
||||||
|
var btn = document.getElementById("save");
|
||||||
|
var id = document.getElementById("petid").value;
|
||||||
|
var pet = {
|
||||||
|
name: document.getElementById("pname").value.trim(),
|
||||||
|
species: document.getElementById("pspecies").value,
|
||||||
|
breed: document.getElementById("pbreed").value.trim(),
|
||||||
|
appearance: document.getElementById("pappear").value.trim(),
|
||||||
|
personality: document.getElementById("pperson").value.trim()
|
||||||
|
};
|
||||||
|
if (!pet.name) { showMsg("msg", "Your pet needs a name."); return; }
|
||||||
|
|
||||||
|
btn.disabled = true; btn.textContent = "Saving…";
|
||||||
|
var r = id
|
||||||
|
? await api("update_pet", { email: email, pet_id: id, pet: pet })
|
||||||
|
: await api("save_pet", { email: email, pet: pet });
|
||||||
|
btn.disabled = false; btn.textContent = "Save pet";
|
||||||
|
if (!r.ok) { showMsg("msg", r.error || "That did not save."); return; }
|
||||||
|
resetForm();
|
||||||
|
showMsg("msg", id ? "Saved." : "Added. You can upload photos below.", "ok");
|
||||||
|
await refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
document.getElementById("petid").value = "";
|
||||||
|
["pname", "pbreed", "pappear", "pperson"].forEach(function (i) { document.getElementById(i).value = ""; });
|
||||||
|
document.getElementById("pspecies").value = "dog";
|
||||||
|
document.getElementById("formtitle").textContent = "Add a pet";
|
||||||
|
document.getElementById("cancel").style.display = "none";
|
||||||
|
}
|
||||||
|
document.getElementById("cancel").addEventListener("click", resetForm);
|
||||||
|
|
||||||
|
document.getElementById("list").addEventListener("click", async function (e) {
|
||||||
|
var ed = e.target.closest("[data-edit]");
|
||||||
|
if (ed) {
|
||||||
|
var p = pets.filter(function (x) { return x.id === ed.getAttribute("data-edit"); })[0];
|
||||||
|
if (!p) return;
|
||||||
|
document.getElementById("petid").value = p.id;
|
||||||
|
document.getElementById("pname").value = p.name || "";
|
||||||
|
document.getElementById("pspecies").value = p.species || "dog";
|
||||||
|
document.getElementById("pbreed").value = p.breed || "";
|
||||||
|
document.getElementById("pappear").value = p.appearance || "";
|
||||||
|
document.getElementById("pperson").value = p.personality || "";
|
||||||
|
document.getElementById("formtitle").textContent = "Edit " + (p.name || "pet");
|
||||||
|
document.getElementById("cancel").style.display = "";
|
||||||
|
document.getElementById("formcard").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var del = e.target.closest("[data-del]");
|
||||||
|
if (del) {
|
||||||
|
if (!confirm("Remove this photo?")) return;
|
||||||
|
var r = await api("delete_photo", { email: email, photo_id: del.getAttribute("data-del") });
|
||||||
|
if (!r.ok) { showMsg("msg", r.error || "Could not remove that photo."); return; }
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("list").addEventListener("change", async function (e) {
|
||||||
|
var f = e.target.closest("[data-file]");
|
||||||
|
if (!f || !f.files || !f.files[0]) return;
|
||||||
|
var petId = f.getAttribute("data-file");
|
||||||
|
var kindSel = document.querySelector('[data-kind="' + petId + '"]');
|
||||||
|
var status = document.querySelector('[data-status="' + petId + '"]');
|
||||||
|
var file = f.files[0];
|
||||||
|
hideMsg("msg");
|
||||||
|
|
||||||
|
if (!/^image\//.test(file.type)) { showMsg("msg", "Please choose an image file."); f.value = ""; return; }
|
||||||
|
status.textContent = "preparing…";
|
||||||
|
var img;
|
||||||
|
try { img = await resizeImage(file, 1600, 0.85); }
|
||||||
|
catch (err) { status.textContent = ""; showMsg("msg", err.message || "Could not read that image."); f.value = ""; return; }
|
||||||
|
|
||||||
|
status.textContent = "uploading…";
|
||||||
|
var r = await api("add_photo", {
|
||||||
|
email: email, pet_id: petId,
|
||||||
|
kind: kindSel ? kindSel.value : "other",
|
||||||
|
data_b64: img.b64
|
||||||
|
});
|
||||||
|
f.value = "";
|
||||||
|
status.textContent = "";
|
||||||
|
if (!r.ok) { showMsg("msg", r.error || "That upload did not work."); return; }
|
||||||
|
await refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in a new issue