paytolaugh/pets.html

224 lines
10 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>
<div class="field">
<label for="pvoice">How they sound</label>
<span class="hint">This is what keeps them the same character across every video. Be specific: age, accent, pace — and anything they should <em>never</em> sound like. Vague descriptions drift between episodes.</span>
<textarea id="pvoice" placeholder="A bright, quick, articulate young adult male voice. Neutral American, no rural or Southern accent, no drawl. Crisp diction, slightly faster than average, confident and a little nerdy."></textarea>
</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 &middot; 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>' : '')
+ (p.voice
? '<p style="color:var(--muted);font-size:13.5px;margin:10px 0 0"><strong style="color:var(--ink)">Voice:</strong> ' + esc(p.voice) + '</p>'
: '<p style="color:var(--accent2);font-size:13.5px;margin:10px 0 0">No voice set, so they will sound like a different character in every video.</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(),
voice: document.getElementById("pvoice").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", "pvoice"].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("pvoice").value = p.voice || "";
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>