feat(sales): add quotes tab, PDF viewer, and opportunity sidebar enhancements
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from "svelte";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
export let src: string | null = null;
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let pageCount = 0;
|
||||
let loading = true;
|
||||
let error = "";
|
||||
|
||||
let pdfjsLib: typeof import("pdfjs-dist") | null = null;
|
||||
let currentPdf: any = null;
|
||||
let currentSrc: string | null = null;
|
||||
let renderGeneration = 0;
|
||||
|
||||
// Max canvas dimension to prevent browser OOM
|
||||
const MAX_CANVAS_DIM = 2048;
|
||||
|
||||
async function initPdfJs() {
|
||||
if (!browser || pdfjsLib) return;
|
||||
const lib = await import("pdfjs-dist");
|
||||
// Use fake worker to avoid worker-loading issues with Vite/SvelteKit
|
||||
lib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
"pdfjs-dist/build/pdf.worker.min.mjs",
|
||||
import.meta.url,
|
||||
).toString();
|
||||
pdfjsLib = lib;
|
||||
}
|
||||
|
||||
async function renderPdf(pdfSrc: string) {
|
||||
if (!pdfSrc || !container || !browser) return;
|
||||
|
||||
// Set currentSrc immediately to prevent re-entry
|
||||
currentSrc = pdfSrc;
|
||||
const generation = ++renderGeneration;
|
||||
loading = true;
|
||||
error = "";
|
||||
|
||||
// Clean up previous PDF
|
||||
if (currentPdf) {
|
||||
currentPdf.destroy();
|
||||
currentPdf = null;
|
||||
}
|
||||
container.innerHTML = "";
|
||||
|
||||
try {
|
||||
await initPdfJs();
|
||||
if (!pdfjsLib || generation !== renderGeneration) return;
|
||||
|
||||
const pdf = await pdfjsLib.getDocument({ url: pdfSrc, isEvalSupported: false }).promise;
|
||||
if (generation !== renderGeneration) {
|
||||
pdf.destroy();
|
||||
return;
|
||||
}
|
||||
currentPdf = pdf;
|
||||
pageCount = pdf.numPages;
|
||||
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
if (generation !== renderGeneration) return;
|
||||
|
||||
const page = await pdf.getPage(i);
|
||||
|
||||
// Calculate a safe scale that won't exceed MAX_CANVAS_DIM
|
||||
const baseViewport = page.getViewport({ scale: 1.0 });
|
||||
const maxDim = Math.max(baseViewport.width, baseViewport.height);
|
||||
const safeScale = maxDim > MAX_CANVAS_DIM ? MAX_CANVAS_DIM / maxDim : 1.0;
|
||||
const viewport = page.getViewport({ scale: safeScale });
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.className = "pdf-viewer-page";
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
canvas.addEventListener("contextmenu", (e) => e.preventDefault());
|
||||
container.appendChild(canvas);
|
||||
|
||||
await page.render({ canvas, viewport } as any).promise;
|
||||
|
||||
// Release page resources after rendering
|
||||
page.cleanup();
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (generation === renderGeneration) {
|
||||
error = err instanceof Error ? err.message : "Failed to render PDF";
|
||||
console.error("PDF render error:", err);
|
||||
}
|
||||
} finally {
|
||||
if (generation === renderGeneration) {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
renderGeneration++;
|
||||
if (currentPdf) {
|
||||
currentPdf.destroy();
|
||||
currentPdf = null;
|
||||
}
|
||||
currentSrc = null;
|
||||
}
|
||||
|
||||
// React to src changes (also handles initial render once container is bound)
|
||||
$: if (browser && src && src !== currentSrc && container) {
|
||||
renderPdf(src);
|
||||
}
|
||||
|
||||
// Handle no-src case
|
||||
$: if (browser && !src) {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
cleanup();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="pdf-viewer"
|
||||
on:contextmenu|preventDefault
|
||||
role="document"
|
||||
>
|
||||
{#if loading}
|
||||
<div class="pdf-viewer-loading">
|
||||
<p>Rendering PDF...</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="pdf-viewer-error">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="pdf-viewer-pages" bind:this={container}></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pdf-viewer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: var(--bg-surface-raised, #2a2a2a);
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pdf-viewer-pages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.pdf-viewer-pages :global(.pdf-viewer-page) {
|
||||
max-width: 100%;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.pdf-viewer-loading,
|
||||
.pdf-viewer-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted, #888);
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user