refactor(tapops): modularize page code

This commit is contained in:
Dodo
2026-06-04 18:13:23 +07:00
parent 529693cd5f
commit 1270ce60fd
41 changed files with 4678 additions and 4476 deletions
+130
View File
@@ -0,0 +1,130 @@
import { ScriptLoader } from './script-loader.js';
import { ViewerApi } from './viewer-api.js';
import { ViewerProgressBar } from './viewer-progress-bar.js';
export class FileViewer {
constructor() {
const params = new URLSearchParams(window.location.search);
this.id = params.get('id');
this.name = params.get('name') || 'Dokumen';
this.type = (params.get('type') || 'pdf').toLowerCase();
this.titleEl = document.getElementById('viewer-title');
this.typeBadgeEl = document.getElementById('viewer-type');
this.downloadEl = document.getElementById('viewer-download');
this.contentEl = document.getElementById('viewer-content');
this.assetUrl = this.id ? ViewerApi.assetUrl(this.id) : null;
}
async init() {
if (!this.id) {
this._renderError('Parameter berkas tidak ditemukan di URL.');
return;
}
document.title = `${this.name} — Pratinjau Dokumen`;
this.titleEl.textContent = this.name;
this.typeBadgeEl.textContent = this.type.toUpperCase();
this._setupDownload();
try {
if (this.type === 'docx') {
await this._renderDocx();
} else {
this._renderPdf();
}
} catch (err) {
console.error(err);
this._renderError('Gagal memuat pratinjau berkas. Silakan unduh berkas untuk membuka secara lokal.');
}
}
_setupDownload() {
const ext = `.${this.type}`;
const filename = this.name.toLowerCase().endsWith(ext) ? this.name : `${this.name}${ext}`;
this.downloadEl.removeAttribute('href');
this.downloadEl.style.cursor = 'pointer';
this.downloadEl.addEventListener('click', async event => {
event.preventDefault();
const originalHtml = this.downloadEl.innerHTML;
this.downloadEl.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Mengunduh…';
this.downloadEl.style.pointerEvents = 'none';
try {
const blob = await ViewerApi.fetchBlob(this.assetUrl);
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.style.display = 'none';
link.href = blobUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(blobUrl);
} catch (err) {
console.warn('Blob download failed, falling back:', err);
window.open(`${this.assetUrl}?download`, '_blank');
} finally {
this.downloadEl.innerHTML = originalHtml;
this.downloadEl.style.pointerEvents = '';
}
});
}
_renderPdf() {
const iframe = document.createElement('iframe');
iframe.className = 'viewer-pdf-frame';
iframe.title = this.name;
iframe.src = this.assetUrl;
this.contentEl.replaceChildren(iframe);
}
async _renderDocx() {
const progressBar = new ViewerProgressBar(this.contentEl);
const [blob] = await Promise.all([
ViewerApi.fetchBlob(this.assetUrl, ratio => progressBar.update(ratio)),
this._loadDocxLibraries(),
]);
progressBar.finish();
if (!window.docx || typeof window.docx.renderAsync !== 'function') {
throw new Error('Library docx-preview belum dimuat.');
}
const container = document.createElement('div');
container.className = 'viewer-docx-content';
this.contentEl.replaceChildren(container);
await window.docx.renderAsync(blob, container, null, {
inWrapper: true,
ignoreWidth: false,
ignoreHeight: false,
});
}
async _loadDocxLibraries() {
if (window.docx) return;
await ScriptLoader.load('https://unpkg.com/jszip@3.10.1/dist/jszip.min.js');
await ScriptLoader.load('https://unpkg.com/docx-preview@0.3.5/dist/docx-preview.js');
}
_renderError(message) {
const wrapper = document.createElement('div');
wrapper.className = 'viewer-error';
const icon = document.createElement('i');
icon.className = 'fas fa-exclamation-triangle';
const span = document.createElement('span');
span.textContent = message;
const back = document.createElement('a');
back.href = 'index.html#repository';
back.className = 'btn-back-mini';
back.textContent = 'Kembali ke Repositori';
wrapper.append(icon, span, back);
this.contentEl.replaceChildren(wrapper);
}
}
+11
View File
@@ -0,0 +1,11 @@
export class ScriptLoader {
static load(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = () => reject(new Error(`Gagal memuat skrip: ${src}`));
document.head.appendChild(script);
});
}
}
+31
View File
@@ -0,0 +1,31 @@
export class ViewerApi {
static API_BASE = 'https://api.ifuntanhub.dev';
static assetUrl(uuid) {
return `${this.API_BASE}/assets/${uuid}`;
}
static async fetchBlob(url, onProgress) {
const response = await fetch(url);
if (!response.ok) throw new Error(`Gagal mengambil berkas (HTTP ${response.status}).`);
const contentLength = response.headers.get('Content-Length');
if (onProgress && contentLength && response.body) {
const total = Number.parseInt(contentLength, 10);
const reader = response.body.getReader();
const chunks = [];
let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
onProgress(received / total);
}
return new Blob(chunks);
}
return response.blob();
}
}
+8
View File
@@ -0,0 +1,8 @@
import { FileViewer } from './file-viewer.js';
import { ViewerPrefetch } from './viewer-prefetch.js';
ViewerPrefetch.fromLocation();
document.addEventListener('DOMContentLoaded', () => {
new FileViewer().init();
});
@@ -0,0 +1,16 @@
import { ViewerApi } from './viewer-api.js';
export class ViewerPrefetch {
static fromLocation(location = window.location) {
const params = new URLSearchParams(location.search);
const id = params.get('id');
if (!id) return;
const link = document.createElement('link');
link.rel = 'prefetch';
link.as = 'fetch';
link.crossOrigin = 'anonymous';
link.href = ViewerApi.assetUrl(id);
document.head.appendChild(link);
}
}
@@ -0,0 +1,19 @@
export class ViewerProgressBar {
constructor(contentEl) {
this.bar = document.createElement('div');
this.bar.className = 'viewer-progress-bar';
this.fill = document.createElement('div');
this.fill.className = 'viewer-progress-fill';
this.bar.appendChild(this.fill);
contentEl.prepend(this.bar);
}
update(ratio) {
this.fill.style.width = `${Math.min(Math.round(ratio * 100), 100)}%`;
}
finish() {
this.fill.style.width = '100%';
setTimeout(() => this.bar.remove(), 300);
}
}