214 lines
9.4 KiB
HTML
214 lines
9.4 KiB
HTML
<!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>
|