212 lines
6.4 KiB
TypeScript
212 lines
6.4 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;
|
|
const btnsubmit = form.querySelector("sl-button[type='submit']") as SlButton;
|
|
|
|
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) {
|
|
btnsubmit.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,
|
|
);
|
|
}
|
|
btnsubmit.loading = false;
|
|
if (r) {
|
|
if (r.ok) {
|
|
notify(
|
|
"Successfully updated transaction",
|
|
undefined,
|
|
undefined,
|
|
null,
|
|
);
|
|
window.location.href = "/transactions";
|
|
} 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() {
|
|
btnsubmit.disabled = true;
|
|
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 () => {
|
|
btnsubmit.disabled = false;
|
|
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;
|
|
});
|