receipts/js/transaction.ts

204 lines
6.1 KiB
TypeScript

import "./transaction.css";
import "@shoelace-style/shoelace/dist/components/button/button.js";
import "@shoelace-style/shoelace/dist/components/breadcrumb/breadcrumb.js";
import "@shoelace-style/shoelace/dist/components/breadcrumb-item/breadcrumb-item.js";
import "@shoelace-style/shoelace/dist/components/details/details.js";
import "@shoelace-style/shoelace/dist/components/icon-button/icon-button.js";
import "@shoelace-style/shoelace/dist/components/input/input.js";
import "@shoelace-style/shoelace/dist/components/textarea/textarea.js";
import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js";
import SlButton from "@shoelace-style/shoelace/dist/components/button/button.js";
import SlDetails from "@shoelace-style/shoelace/dist/components/details/details.js";
import SlIconButton from "@shoelace-style/shoelace/dist/components/icon-button/icon-button.js";
import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
import Cropper from "cropperjs";
import { notify } from "./alert";
setBasePath("/static/shoelace/");
const form = document.forms[0];
const photobox = document.getElementById("photo-box")!;
const photoview = document.getElementById("photo-view")!;
const video = photoview.querySelector("video")!;
const btnshutter = photoview.querySelector(
" sl-icon-button[label='Take Photo']",
) as SlIconButton;
const btncrop = photoview.querySelector(
" sl-icon-button[label='Crop']",
) as SlIconButton;
const btnreset = photoview.querySelector(
" sl-icon-button[label='Start Over']",
) as SlIconButton;
let cropper: Cropper | null = null;
let initialized = false;
async function clearCamera() {
if (cropper) {
cropper.getCropperCanvas()?.remove();
cropper = null;
}
video.pause();
video.srcObject = null;
video.classList.add("invisible");
video.parentNode?.querySelectorAll("canvas").forEach((e) => e.remove());
btnshutter.disabled = true;
btnshutter.classList.add("invisible");
btncrop.disabled = true;
btncrop.classList.add("invisible");
btnreset.disabled = true;
btnreset.classList.add("invisible");
photoview.classList.remove("invisible");
}
async function startCamera() {
let stream: MediaStream;
try {
stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: {
ideal: "environment",
},
},
audio: false,
});
} catch (ex) {
console.error(ex);
notify(`${ex}`, "danger", "exclamation-octagon", null);
return;
}
photobox.querySelectorAll(".fallback").forEach((e) => e.remove());
btnshutter.classList.remove("invisible");
video.classList.remove("invisible");
video.srcObject = stream;
video.play();
}
async function submitForm(data: FormData) {
const btn = form.querySelector("sl-button[type='submit']") as SlButton;
btn.loading = true;
let r: Response | null = null;
try {
r = await fetch("", {
method: "POST",
body: data,
});
} catch (e) {
notify(
`Failed to submit form: ${e}`,
"danger",
"exclamation-octagon",
null,
);
}
btn.loading = false;
if (r) {
if (r.ok) {
notify("Successfully updated transaction");
} else {
const html = await r.text();
if (html) {
const doc = new DOMParser().parseFromString(html, "text/html");
notify(
doc.body.textContent ?? "",
"danger",
"exclamation-octagon",
null,
);
} else {
notify(r.statusText, "danger", "exclamation-octagon", null);
}
}
}
}
function takePhoto() {
btnshutter.disabled = true;
btnshutter.classList.add("invisible");
video.pause();
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
if (!context) {
notify(
"Failed to get canvas 2D rendering context",
"danger",
"exclamation-octagon",
null,
);
return;
}
const width = video.videoWidth;
const height = video.videoHeight;
canvas.width = width;
canvas.height = height;
context.drawImage(video, 0, 0, width, height);
video.srcObject = null;
video.classList.add("invisible");
video.parentNode!.appendChild(canvas);
cropper = new Cropper(canvas);
cropper.getCropperCanvas()!.style.height = `${height}px`;
btncrop.disabled = false;
btncrop.classList.remove("invisible");
btnreset.disabled = false;
btnreset.classList.remove("invisible");
}
photobox.addEventListener("sl-show", () => {
if (!initialized) {
initialized = true;
clearCamera();
startCamera();
}
});
video.addEventListener("canplay", () => {
btnshutter.disabled = false;
});
btnshutter.addEventListener("click", async () => {
takePhoto();
});
btncrop.addEventListener("click", async () => {
if (cropper) {
const canvas = await cropper.getCropperSelection()?.$toCanvas();
if (canvas) {
canvas.setAttribute("id", "camera-photo");
video.parentNode!.appendChild(canvas);
cropper.getCropperCanvas()?.remove();
btncrop.disabled = true;
btncrop.classList.add("invisible");
cropper = null;
}
}
});
btnreset.addEventListener("click", () => {
clearCamera();
startCamera();
});
form.addEventListener("submit", async (evt) => {
evt.preventDefault();
let data = new FormData(form);
const canvas = document.getElementById("camera-photo") as HTMLCanvasElement;
if (canvas) {
canvas.toBlob((blob: Blob | null) => {
if (blob) {
data.append("photo", blob, "photo.jpg");
}
submitForm(data);
}, "image/jpeg");
} else {
submitForm(data);
}
});
form.addEventListener("reset", () => {
document.querySelectorAll("sl-details").forEach((e: SlDetails) => e.hide());
clearCamera();
initialized = false;
});