fix(journals): resolve Svelte parser error in export modal
- Replaced directive with string interpolation for Tailwind classes containing colons and slashes (e.g. ), which caused build failures.
This commit is contained in:
171
src/routes/journals/ae_comp__modal_journal_export.svelte
Normal file
171
src/routes/journals/ae_comp__modal_journal_export.svelte
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* @file ae_comp__modal_journal_export.svelte
|
||||||
|
* @description Modal component for bulk exporting journal entries.
|
||||||
|
* Allows exporting the currently filtered list of entries to Markdown, HTML, or JSON.
|
||||||
|
* Supports both downloading as a file and copying to clipboard.
|
||||||
|
* @author One Sky IT
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Modal } from 'flowbite-svelte';
|
||||||
|
import { Download, Copy, FileJson, FileType, Code } from '@lucide/svelte';
|
||||||
|
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||||
|
import type { ae_JournalEntry } from '$lib/types/ae_types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
entries: ae_JournalEntry[];
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { open = $bindable(false), entries = [], onClose }: Props = $props();
|
||||||
|
|
||||||
|
// State
|
||||||
|
let export_format: 'markdown' | 'html' | 'json' = $state('markdown');
|
||||||
|
let export_preview: string = $state('');
|
||||||
|
let export_count: number = $derived(entries.length);
|
||||||
|
|
||||||
|
// Re-generate preview when entries or format changes
|
||||||
|
$effect(() => {
|
||||||
|
if (open && entries.length > 0) {
|
||||||
|
generate_preview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the export string based on the selected format.
|
||||||
|
*/
|
||||||
|
function generate_preview() {
|
||||||
|
if (export_format === 'json') {
|
||||||
|
// Simple JSON dump
|
||||||
|
export_preview = JSON.stringify(entries, null, 2);
|
||||||
|
} else if (export_format === 'html') {
|
||||||
|
// HTML: Concatenate rendered content with semantic separators
|
||||||
|
export_preview = entries.map(entry => {
|
||||||
|
const dateStr = ae_util.iso_datetime_formatter(entry.created_on, 'datetime_12_long');
|
||||||
|
const title = entry.name || dateStr;
|
||||||
|
return `
|
||||||
|
<article class="journal-entry" id="entry-${entry.journal_entry_id}">
|
||||||
|
<header>
|
||||||
|
<h1>${title}</h1>
|
||||||
|
<time datetime="${entry.created_on}">${dateStr}</time>
|
||||||
|
</header>
|
||||||
|
<div class="content">
|
||||||
|
${entry.content_md_html || ''}
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
</article>`;
|
||||||
|
}).join('\n\n');
|
||||||
|
} else {
|
||||||
|
// Markdown: Standard concatenation
|
||||||
|
export_preview = entries.map(entry => {
|
||||||
|
const dateStr = ae_util.iso_datetime_formatter(entry.created_on, 'datetime_12_long');
|
||||||
|
const title = entry.name || dateStr;
|
||||||
|
// Ensure we don't double up headers if the content already starts with one
|
||||||
|
let content = entry.content || '';
|
||||||
|
const header = `# ${title}\n*${dateStr}*\n\n`;
|
||||||
|
return `${header}${content}\n\n---\n`;
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads the generated content as a file.
|
||||||
|
*/
|
||||||
|
function handle_download() {
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const filename = `journal_export_${timestamp}.${export_format === 'markdown' ? 'md' : export_format}`;
|
||||||
|
const blob = new Blob([export_preview], { type: 'text/plain;charset=utf-8' });
|
||||||
|
|
||||||
|
// Create temporary link to trigger download
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the generated content to the clipboard.
|
||||||
|
*/
|
||||||
|
async function handle_copy() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(export_preview);
|
||||||
|
alert('Content copied to clipboard!');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy:', err);
|
||||||
|
alert('Failed to copy to clipboard.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="Export Journal Entries"
|
||||||
|
bind:open={open}
|
||||||
|
autoclose={false}
|
||||||
|
size="xl"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="p-4 bg-blue-50 dark:bg-blue-900/20 text-blue-800 dark:text-blue-200 rounded-lg">
|
||||||
|
<strong>Ready to Export:</strong> {export_count} entries currently filtered/visible.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Format Selection -->
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
<button
|
||||||
|
class="btn variant-ringed-surface flex flex-col gap-2 h-24 items-center justify-center border-2 {export_format === 'markdown' ? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20' : ''}"
|
||||||
|
onclick={() => { export_format = 'markdown'; generate_preview(); }}
|
||||||
|
>
|
||||||
|
<FileType size="2em" />
|
||||||
|
<span class="font-bold">Markdown</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn variant-ringed-surface flex flex-col gap-2 h-24 items-center justify-center border-2 {export_format === 'html' ? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20' : ''}"
|
||||||
|
onclick={() => { export_format = 'html'; generate_preview(); }}
|
||||||
|
>
|
||||||
|
<Code size="2em" />
|
||||||
|
<span class="font-bold">HTML</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn variant-ringed-surface flex flex-col gap-2 h-24 items-center justify-center border-2 {export_format === 'json' ? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20' : ''}"
|
||||||
|
onclick={() => { export_format = 'json'; generate_preview(); }}
|
||||||
|
>
|
||||||
|
<FileJson size="2em" />
|
||||||
|
<span class="font-bold">JSON</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview Area -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Preview</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
class="textarea h-64 font-mono text-xs bg-gray-50 dark:bg-gray-900"
|
||||||
|
readonly
|
||||||
|
value={export_preview.substring(0, 5000) + (export_preview.length > 5000 ? '\n... (truncated for preview)' : '')}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="modal-action flex justify-between items-center">
|
||||||
|
<button class="btn preset-tonal-secondary" onclick={onClose}>Close</button>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button class="btn variant-soft-primary" onclick={handle_copy}>
|
||||||
|
<Copy class="mr-2" size="1.2em" /> Copy to Clipboard
|
||||||
|
</button>
|
||||||
|
<button class="btn preset-filled-primary" onclick={handle_download}>
|
||||||
|
<Download class="mr-2" size="1.2em" /> Download File
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
Reference in New Issue
Block a user