Coreccion del cliente para descargar los archivos multimedia

This commit is contained in:
2026-02-24 14:00:28 -06:00
parent c9f934a0da
commit ad26062f46
6 changed files with 114 additions and 24 deletions

View File

@@ -16,6 +16,25 @@
import { marked } from 'marked';
import DOMPurify from 'dompurify';
// Helper to resolve an API path from the DB (e.g., /api/uploads/...) to a full URL pointing to the local backend.
function resolveAssetUrl(url: string | undefined): string {
try {
if (!url) return '';
if (url.startsWith('http://') || url.startsWith('https://')) return url;
// Fallback to empty string if VITE_API_URL is missing
// @ts-ignore
const apiUrl = import.meta.env.VITE_API_URL || '';
const base = apiUrl.replace(/\/$/, '');
const cleanUrl = url.replace(/^\/api\//, '/');
const finalUrl = `${base}${cleanUrl}`;
return finalUrl;
} catch (e) {
console.error('[SSR DEBUG] Error in resolveAssetUrl:', e);
return '';
}
}
let article: HelpArticle | null = null; // Removed $state() as it's not reactive by itself in Svelte 5 standard store usage unless wrapped
let loading = true; // Use standard value, reactive via variable assignment in Svelte 5 runes if enabled
let error: string | null = null;
@@ -60,16 +79,32 @@
import { browser } from '$app/environment';
function renderMarkdown(content: string) {
const rawHtml = marked.parse(content || '') as string;
// Add IDs to headers for TOC navigation
const htmlWithIds = rawHtml.replace(/<h([1-3])>(.*?)<\/h\1>/g, (match, level, text) => {
const id = text.toLowerCase().replace(/[^\w]+/g, '-');
return `<h${level} id="${id}">${text}</h${level}>`;
});
if (browser) {
return DOMPurify.sanitize(htmlWithIds);
if (!content) return '';
try {
const rawHtml = marked.parse(content) as string;
// Add IDs to headers for TOC navigation
let htmlWithIds = rawHtml.replace(/<h([1-3])>(.*?)<\/h\1>/g, (match, level, text) => {
const id = text.toLowerCase().replace(/[^\w]+/g, '-');
return `<h${level} id="${id}">${text}</h${level}>`;
});
// Rewrite image src and link hrefs to point to the local backend
htmlWithIds = htmlWithIds.replace(/src="([^"]+)"/g, (match, src) => {
return `src="${resolveAssetUrl(src)}"`;
});
htmlWithIds = htmlWithIds.replace(/href="(\/api\/uploads\/[^"]+)"/g, (match, href) => {
return `href="${resolveAssetUrl(href)}"`;
});
if (browser && typeof window !== 'undefined') {
// DOMPurify only works in the browser
return DOMPurify.sanitize(htmlWithIds);
}
return htmlWithIds; // SSR returns raw HTML (or could use isomorphic-dompurify)
} catch (e) {
console.error('Markdown parsing error:', e);
return '';
}
return htmlWithIds;
}
function scrollToHeader(id: string) {
@@ -152,11 +187,20 @@
</div>
</div>
<div class="flex gap-2">
<Button variant="outline" size="sm" href={article.file_url} target="_blank">
<Button
variant="outline"
size="sm"
href={resolveAssetUrl(article.file_url)}
target="_blank"
>
<ExternalLink class="mr-2 h-4 w-4" />
Abrir en pestaña
</Button>
<Button size="sm" href={article.file_url} download={article.title}>
<Button
size="sm"
href={resolveAssetUrl(article.file_url)}
download={article.title}
>
<Download class="mr-2 h-4 w-4" />
Descargar
</Button>
@@ -164,7 +208,7 @@
</div>
<!-- PDF Embed -->
<iframe
src={article.file_url}
src={resolveAssetUrl(article.file_url)}
title={article.title}
class="h-[800px] w-full rounded-xl border shadow-inner"
></iframe>
@@ -172,7 +216,7 @@
{:else if article.content_type === 'video'}
<div class="flex flex-col gap-4">
<video controls class="w-full rounded-xl border shadow-lg">
<source src={article.file_url} type={article.mime_type} />
<source src={resolveAssetUrl(article.file_url)} type={article.mime_type} />
Tu navegador no soporta videos.
</video>
<div class="rounded-lg border bg-muted/30 p-4">
@@ -193,7 +237,11 @@
Este archivo no tiene vista previa directa.
</p>
</div>
<Button href={article.file_url} download={article.title} class="px-8 shadow-lg">
<Button
href={resolveAssetUrl(article.file_url)}
download={article.title}
class="px-8 shadow-lg"
>
<Download class="mr-2 h-4 w-4" />
Descargar ahora
</Button>

View File

@@ -27,6 +27,19 @@
import { marked } from 'marked';
import DOMPurify from 'dompurify';
// Helper to resolve an API path from the DB (e.g., /api/uploads/...) to a full URL pointing to the local backend.
function resolveAssetUrl(url: string | undefined): string {
if (!url) return '';
if (url.startsWith('http://') || url.startsWith('https://')) return url;
// Fallback to empty string if VITE_API_URL is missing
// @ts-ignore
const apiUrl = import.meta.env.VITE_API_URL || '';
const base = apiUrl.replace(/\/$/, '');
const cleanUrl = url.replace(/^\/api\//, '/');
return `${base}${cleanUrl}`;
}
let article = $state<HelpArticle | null>(null);
let loading = $state(true);
let processing = $state(false);
@@ -206,11 +219,26 @@
}
function renderMarkdown(text: string) {
const html = marked.parse(text || '') as string;
if (browser) {
return DOMPurify.sanitize(html);
if (!text) return '';
try {
const html = marked.parse(text) as string;
// Rewrite image src and link hrefs to point to the local backend
let resolvedHtml = html.replace(/src="([^"]+)"/g, (match, src) => {
return `src="${resolveAssetUrl(src)}"`;
});
resolvedHtml = resolvedHtml.replace(/href="(\/api\/uploads\/[^"]+)"/g, (match, href) => {
return `href="${resolveAssetUrl(href)}"`;
});
if (browser && typeof window !== 'undefined') {
return DOMPurify.sanitize(resolvedHtml);
}
return resolvedHtml;
} catch (e) {
console.error("Markdown parsing error in editor:", e);
return '';
}
return html;
}
</script>
@@ -359,8 +387,8 @@
bind:value={content}
class="w-full flex-1 resize-none bg-background p-8 font-mono text-sm leading-relaxed outline-none"
placeholder="# Empieza a escribir aquí..."
ondrop={handleDrop}
ondragover={(e) => e.preventDefault()}
ondrop={(e: DragEvent) => handleDrop(e)}
ondragover={(e: DragEvent) => e.preventDefault()}
></textarea>
<!-- Dropping Hint -->
@@ -418,7 +446,7 @@
</div>
</div>
<div class="flex gap-2">
<Button variant="outline" class="flex-1" href={file_url} target="_blank">
<Button variant="outline" class="flex-1" href={resolveAssetUrl(file_url)} target="_blank">
Ver Archivo
</Button>
<label