Skip to main content

max / makenotwork

4.2 KB · 108 lines History Blame Raw
1 // <mnw-image-uploader> — single-image upload with preview, unifying the
2 // byte-near-identical static/wizard-item-basics.js and
3 // wizard-project-appearance.js. Entity-agnostic via data attributes:
4 //
5 // data-presign-url POST endpoint returning { upload_url, s3_key, cache_control }
6 // data-confirm-url POST endpoint returning { image_url }
7 // data-id-field body field naming the entity, e.g. "item_id" / "project_id"
8 // data-id-value the entity id
9 // data-alt alt text for the rendered image (optional)
10 // data-preview-target id of an element to also fill with the image (optional)
11 //
12 // A controller element (carries only the data-* config): it wires the
13 // page's dropzone markup by id, so there's one uploader per wizard step.
14
15 import { MnwElement, define } from '../base.ts';
16 import { S3Uploader, initDropzone, presignPutConfirm } from './s3.ts';
17
18 class ImageUploader extends MnwElement {
19 protected init(): void {
20 const { presignUrl, confirmUrl, idField, idValue } = this.dataset;
21 if (!presignUrl || !confirmUrl || !idField || idValue === undefined) return;
22 const alt = this.dataset.alt || 'Image';
23 const previewTargetId = this.dataset.previewTarget;
24
25 const pick = <T extends HTMLElement>(id: string): T | null => document.getElementById(id) as T | null;
26 const dropzone = pick<HTMLElement>('image-dropzone');
27 const fileInput = pick<HTMLInputElement>('image-file-input');
28 if (!dropzone || !fileInput) return;
29 const hiddenUrl = pick<HTMLInputElement>('cover-image-url');
30 const progressEl = pick('image-upload-progress');
31 const errorEl = pick('image-upload-error');
32 const placeholder = pick('image-placeholder');
33 const chooseBtn = pick<HTMLButtonElement>('choose-image-btn');
34 let currentImg = pick<HTMLElement>('image-current');
35
36 const uploader = new S3Uploader({
37 filenameEl: pick('image-upload-filename'),
38 percentEl: pick('image-upload-percent'),
39 progressBar: pick('image-progress-bar'),
40 });
41
42 const showError = (message: string): void => {
43 errorEl?.classList.remove('hidden');
44 const m = pick('image-error-message');
45 if (m) m.textContent = message;
46 };
47
48 const renderImg = (src: string, altText: string): HTMLImageElement => {
49 const img = document.createElement('img');
50 img.src = src;
51 img.alt = altText;
52 img.className = 'wizard-cover-img';
53 return img;
54 };
55
56 const showUploadedImage = (url: string): void => {
57 placeholder?.classList.add('hidden');
58 if (currentImg) {
59 const img = currentImg.querySelector('img');
60 if (img) img.src = url;
61 currentImg.classList.remove('hidden');
62 } else {
63 const div = document.createElement('div');
64 div.className = 'project-image-current';
65 div.id = 'image-current';
66 div.appendChild(renderImg(url, alt));
67 dropzone.insertBefore(div, fileInput);
68 currentImg = div;
69 }
70 if (previewTargetId) {
71 const preview = document.getElementById(previewTargetId);
72 if (preview) {
73 preview.textContent = '';
74 preview.appendChild(renderImg(url, 'Preview'));
75 }
76 }
77 };
78
79 const upload = async (file: File): Promise<void> => {
80 errorEl?.classList.add('hidden');
81 progressEl?.classList.remove('hidden');
82 if (chooseBtn) chooseBtn.disabled = true;
83 try {
84 const result = await presignPutConfirm<{ image_url: string }>({
85 presignUrl,
86 confirmUrl,
87 presignBody: { [idField]: idValue, file_name: file.name, content_type: file.type || 'image/jpeg' },
88 confirmBody: (s3Key) => ({ [idField]: idValue, s3_key: s3Key }),
89 file,
90 uploader,
91 fallbackContentType: 'image/jpeg',
92 });
93 if (hiddenUrl) hiddenUrl.value = result.image_url;
94 showUploadedImage(result.image_url);
95 } catch (err) {
96 showError(err instanceof Error ? err.message : 'Upload failed. Please try again.');
97 } finally {
98 progressEl?.classList.add('hidden');
99 if (chooseBtn) chooseBtn.disabled = false;
100 }
101 };
102
103 initDropzone(dropzone, fileInput, (file) => void upload(file));
104 }
105 }
106
107 define('mnw-image-uploader', ImageUploader);
108