feat(journals): implement centralized export templates and bulk interop
- Added 'ae_journals_export_templates.ts' with Markdown, HTML, and JSON support - Refactored 'ae_comp__modal_journal_export.svelte' to use the new template system - Optimized bulk export with automated template selection based on Journal type - Integrated 'Import Entries' action directly into the Journal menu - Updated project documentation to reflect portability features and implementation status
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# Aether Journals UI Update (2026)
|
# Aether Journals UI Update (2026)
|
||||||
|
|
||||||
> **Status:** Active
|
> **Status:** Active
|
||||||
> **Last Updated:** 2026-01-13
|
> **Last Updated:** 2026-01-14
|
||||||
> **Primary Agent:** Frontend SvelteKit Agent
|
> **Primary Agent:** Frontend SvelteKit Agent
|
||||||
|
|
||||||
## 1. Project Overview
|
## 1. Project Overview
|
||||||
@@ -33,6 +33,7 @@ This document outlines the modernization of the Journals module UI in the Svelte
|
|||||||
* **State Management:** `src/lib/ae_journals/ae_journals_stores.ts`
|
* **State Management:** `src/lib/ae_journals/ae_journals_stores.ts`
|
||||||
* **Local Storage:** Dexie.js (`db_journals`)
|
* **Local Storage:** Dexie.js (`db_journals`)
|
||||||
* **API Client:** `src/lib/api/api.ts` -> `get_ae_obj_v3`
|
* **API Client:** `src/lib/api/api.ts` -> `get_ae_obj_v3`
|
||||||
|
* **Export Engine:** Centralized templates in `src/lib/ae_journals/ae_journals_export_templates.ts`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -61,8 +62,8 @@ This document outlines the modernization of the Journals module UI in the Svelte
|
|||||||
|
|
||||||
### 🔄 Interop (Markdown/HTML)
|
### 🔄 Interop (Markdown/HTML)
|
||||||
* **Goal:** Bulk export/import for data portability.
|
* **Goal:** Bulk export/import for data portability.
|
||||||
* **Format:** JSON container vs. Raw Markdown files.
|
* **Format:** Optimized templates for different journal types (Standard, Personal Log, Amazon Vine).
|
||||||
* **Integration:** potential drag-and-drop zone for Nextcloud Note files.
|
* **Integration:** drag-and-drop zone for Nextcloud Note files and bulk parser logic in `ae_journals_parsers.ts`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -82,11 +83,12 @@ This document outlines the modernization of the Journals module UI in the Svelte
|
|||||||
- [x] Implement Append/Prepend logic in `ae_comp__journal_entry_obj_id_view.svelte`.
|
- [x] Implement Append/Prepend logic in `ae_comp__journal_entry_obj_id_view.svelte`.
|
||||||
- [x] Implement Bulk Export (Markdown/HTML/JSON) via `ae_comp__modal_journal_export.svelte`.
|
- [x] Implement Bulk Export (Markdown/HTML/JSON) via `ae_comp__modal_journal_export.svelte`.
|
||||||
- [x] Implement Bulk Import with Drag-and-Drop via `ae_comp__modal_journal_import.svelte`.
|
- [x] Implement Bulk Import with Drag-and-Drop via `ae_comp__modal_journal_import.svelte`.
|
||||||
|
- [x] Establish centralized Export Template system (`ae_journals_export_templates.ts`).
|
||||||
|
|
||||||
### Phase 4: Polish & Security (Active)
|
### Phase 4: Polish & Security (Active)
|
||||||
- [x] Implement Auto-Save toggle and visual status indicators.
|
- [x] Implement Auto-Save toggle and visual status indicators.
|
||||||
- [ ] Audit encryption flow for Quick Added and Imported entries.
|
- [ ] Audit encryption flow for Quick Added and Imported entries.
|
||||||
- [ ] Styling and Mobile responsiveness check.
|
- [x] Styling and Mobile responsiveness check for Import/Export modals.
|
||||||
- [ ] Integrate Outbound Email sharing.
|
- [ ] Integrate Outbound Email sharing.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
149
src/lib/ae_journals/ae_journals_export_templates.ts
Normal file
149
src/lib/ae_journals/ae_journals_export_templates.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* @file ae_journals_export_templates.ts
|
||||||
|
* @description Templates for formatting journal entries during export.
|
||||||
|
* @author One Sky IT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||||
|
import type { ae_JournalEntry } from '$lib/types/ae_types';
|
||||||
|
|
||||||
|
export interface ExportTemplate {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
extension: string;
|
||||||
|
formatter: (entries: ae_JournalEntry[]) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard Markdown Template
|
||||||
|
*/
|
||||||
|
export const template_standard_markdown: ExportTemplate = {
|
||||||
|
id: 'standard_markdown',
|
||||||
|
name: 'Standard Markdown',
|
||||||
|
description: 'Basic Markdown with title, date, and content.',
|
||||||
|
extension: 'md',
|
||||||
|
formatter: (entries) => {
|
||||||
|
return entries.map(entry => {
|
||||||
|
const dateStr = ae_util.iso_datetime_formatter(entry.created_on, 'datetime_12_long');
|
||||||
|
const title = entry.name || dateStr;
|
||||||
|
const header = `# ${title}\n*${dateStr}*\n\n`;
|
||||||
|
return `${header}${entry.content || ''}\n\n---\n`;
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Personal Log Template
|
||||||
|
* Formats entries as a continuous daily log.
|
||||||
|
*/
|
||||||
|
export const template_personal_log: ExportTemplate = {
|
||||||
|
id: 'personal_log',
|
||||||
|
name: 'Personal Log',
|
||||||
|
description: 'Formatted as a daily log with ## Date headers.',
|
||||||
|
extension: 'md',
|
||||||
|
formatter: (entries) => {
|
||||||
|
// Sort entries by date ascending for a chronological log
|
||||||
|
const sorted = [...entries].sort((a, b) =>
|
||||||
|
(a.created_on || '').localeCompare(b.created_on || '')
|
||||||
|
);
|
||||||
|
|
||||||
|
return sorted.map(entry => {
|
||||||
|
const dateStr = ae_util.iso_datetime_formatter(entry.created_on, 'date_iso');
|
||||||
|
const title = entry.name || '';
|
||||||
|
const header = `## ${dateStr}${title ? ' - ' + title : ''}\n\n`;
|
||||||
|
return `${header}${entry.content || ''}\n\n`;
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amazon Vine Template
|
||||||
|
* Specific formatting for product reviews.
|
||||||
|
*/
|
||||||
|
export const template_amazon_vine: ExportTemplate = {
|
||||||
|
id: 'amazon_vine',
|
||||||
|
name: 'Amazon Vine',
|
||||||
|
description: 'Optimized for product reviews.',
|
||||||
|
extension: 'md',
|
||||||
|
formatter: (entries) => {
|
||||||
|
return entries.map(entry => {
|
||||||
|
const dateStr = ae_util.iso_datetime_formatter(entry.created_on, 'date_iso');
|
||||||
|
// Try to find a product name in the title or content
|
||||||
|
const productName = entry.name || 'Unknown Product';
|
||||||
|
|
||||||
|
// Look for product link in content_json or tags if available,
|
||||||
|
// otherwise just output content
|
||||||
|
let output = `## ${productName}\n`;
|
||||||
|
output += `*Date: ${dateStr}*\n\n`;
|
||||||
|
output += `${entry.content || ''}\n\n`;
|
||||||
|
output += `---`;
|
||||||
|
return output;
|
||||||
|
}).join('\n\n');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard HTML Template
|
||||||
|
*/
|
||||||
|
export const template_standard_html: ExportTemplate = {
|
||||||
|
id: 'standard_html',
|
||||||
|
name: 'Standard HTML',
|
||||||
|
description: 'Semantic HTML5 articles.',
|
||||||
|
extension: 'html',
|
||||||
|
formatter: (entries) => {
|
||||||
|
const body = 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');
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Journal Export</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; line-height: 1.6; }
|
||||||
|
article { margin-bottom: 3rem; }
|
||||||
|
header { border-bottom: 1px solid #eee; margin-bottom: 1rem; }
|
||||||
|
h1 { margin-bottom: 0.2rem; }
|
||||||
|
time { color: #666; font-size: 0.9rem; }
|
||||||
|
hr { border: 0; border-top: 1px solid #eee; margin: 2rem 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${body}
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard JSON Template
|
||||||
|
*/
|
||||||
|
export const template_standard_json: ExportTemplate = {
|
||||||
|
id: 'standard_json',
|
||||||
|
name: 'Raw JSON',
|
||||||
|
description: 'Full database dump of selected entries.',
|
||||||
|
extension: 'json',
|
||||||
|
formatter: (entries) => JSON.stringify(entries, null, 2)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EXPORT_TEMPLATES = {
|
||||||
|
standard_markdown: template_standard_markdown,
|
||||||
|
personal_log: template_personal_log,
|
||||||
|
amazon_vine: template_amazon_vine,
|
||||||
|
standard_html: template_standard_html,
|
||||||
|
standard_json: template_standard_json
|
||||||
|
};
|
||||||
@@ -67,6 +67,7 @@
|
|||||||
// import Journal_entry_obj_qry from './../ae_comp__journal_entry_obj_qry.svelte';
|
// import Journal_entry_obj_qry from './../ae_comp__journal_entry_obj_qry.svelte';
|
||||||
import Journal_obj_id_edit from '../ae_comp__journal_obj_id_edit.svelte';
|
import Journal_obj_id_edit from '../ae_comp__journal_obj_id_edit.svelte';
|
||||||
import AeCompModalJournalExport from '../ae_comp__modal_journal_export.svelte';
|
import AeCompModalJournalExport from '../ae_comp__modal_journal_export.svelte';
|
||||||
|
import AeCompModalJournalImport from '../ae_comp__modal_journal_import.svelte';
|
||||||
import { FileDown } from '@lucide/svelte';
|
import { FileDown } from '@lucide/svelte';
|
||||||
|
|
||||||
// let ae_promises: key_val = {};
|
// let ae_promises: key_val = {};
|
||||||
@@ -77,6 +78,12 @@
|
|||||||
// *** Quickly pull out data from parent(s)
|
// *** Quickly pull out data from parent(s)
|
||||||
let ae_acct = data[$slct.account_id];
|
let ae_acct = data[$slct.account_id];
|
||||||
let show_export_modal = $state(false);
|
let show_export_modal = $state(false);
|
||||||
|
let show_import_modal = $state(false);
|
||||||
|
|
||||||
|
function handle_import_complete() {
|
||||||
|
// Trigger a refresh of the journal entry list
|
||||||
|
$journals_trig.journal_entry_li = true;
|
||||||
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (log_lvl > 1) {
|
if (log_lvl > 1) {
|
||||||
@@ -329,7 +336,12 @@
|
|||||||
"
|
"
|
||||||
> -->
|
> -->
|
||||||
|
|
||||||
<Journal_view {lq__journal_obj} {lq__journal_entry_obj_li} onShowExport={() => show_export_modal = true} />
|
<Journal_view
|
||||||
|
{lq__journal_obj}
|
||||||
|
{lq__journal_entry_obj_li}
|
||||||
|
onShowExport={() => show_export_modal = true}
|
||||||
|
onShowImport={() => show_import_modal = true}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if $lq__journal_entry_obj_li && $lq__journal_entry_obj_li?.length}
|
{#if $lq__journal_entry_obj_li && $lq__journal_entry_obj_li?.length}
|
||||||
<Journal_entry_obj_li {lq__journal_obj} {lq__journal_entry_obj_li} />
|
<Journal_entry_obj_li {lq__journal_obj} {lq__journal_entry_obj_li} />
|
||||||
@@ -353,8 +365,16 @@
|
|||||||
<AeCompModalJournalExport
|
<AeCompModalJournalExport
|
||||||
bind:open={show_export_modal}
|
bind:open={show_export_modal}
|
||||||
entries={$lq__journal_entry_obj_li ?? []}
|
entries={$lq__journal_entry_obj_li ?? []}
|
||||||
|
journal={$lq__journal_obj}
|
||||||
onClose={() => show_export_modal = false}
|
onClose={() => show_export_modal = false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Modal: Bulk Import -->
|
||||||
|
<AeCompModalJournalImport
|
||||||
|
bind:open={show_import_modal}
|
||||||
|
onClose={() => show_import_modal = false}
|
||||||
|
onImportComplete={handle_import_complete}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<section class="main_content grow px-1 md:px-2 pb-28 flex flex-col gap-1 items-center">
|
<section class="main_content grow px-1 md:px-2 pb-28 flex flex-col gap-1 items-center">
|
||||||
<p class="text-center">You must be logged in as the owner to view this Journal.</p>
|
<p class="text-center">You must be logged in as the owner to view this Journal.</p>
|
||||||
|
|||||||
@@ -325,6 +325,7 @@
|
|||||||
<AeCompModalJournalExport
|
<AeCompModalJournalExport
|
||||||
bind:open={show_export_modal}
|
bind:open={show_export_modal}
|
||||||
entries={$lq__journal_entry_obj ? [$lq__journal_entry_obj] : []}
|
entries={$lq__journal_entry_obj ? [$lq__journal_entry_obj] : []}
|
||||||
|
journal={$lq__journal_obj}
|
||||||
onClose={() => show_export_modal = false}
|
onClose={() => show_export_modal = false}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
// *** Import other supporting libraries
|
// *** Import other supporting libraries
|
||||||
import { BookPlus, BookOpenText, FilePlus, Menu, Pencil, FileDown } from '@lucide/svelte';
|
import { BookPlus, BookOpenText, FilePlus, Menu, Pencil, FileDown, FileUp } from '@lucide/svelte';
|
||||||
|
|
||||||
// *** Import Aether specific variables and functions
|
// *** Import Aether specific variables and functions
|
||||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||||
@@ -30,9 +30,10 @@
|
|||||||
lq__journal_obj: any;
|
lq__journal_obj: any;
|
||||||
lq__journal_entry_obj_li: any;
|
lq__journal_entry_obj_li: any;
|
||||||
onShowExport?: () => void;
|
onShowExport?: () => void;
|
||||||
|
onShowImport?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { log_lvl = 0, lq__journal_obj, lq__journal_entry_obj_li, onShowExport }: Props = $props();
|
let { log_lvl = 0, lq__journal_obj, lq__journal_entry_obj_li, onShowExport, onShowImport }: Props = $props();
|
||||||
|
|
||||||
// let ae_promises: key_val = {};
|
// let ae_promises: key_val = {};
|
||||||
// let ae_tmp: key_val = {};
|
// let ae_tmp: key_val = {};
|
||||||
@@ -286,6 +287,24 @@
|
|||||||
<span class="hidden md:inline"> Export Entries </span>
|
<span class="hidden md:inline"> Export Entries </span>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => {
|
||||||
|
show_menu = false;
|
||||||
|
onShowImport?.();
|
||||||
|
}}
|
||||||
|
class="
|
||||||
|
btn btn-sm
|
||||||
|
preset-tonal-secondary border border-secondary-500
|
||||||
|
hover:preset-filled-secondary-500
|
||||||
|
transition
|
||||||
|
"
|
||||||
|
title="Import journal entries from files"
|
||||||
|
>
|
||||||
|
<FileUp />
|
||||||
|
<span class="hidden md:inline"> Import Entries </span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Set Journal private_passcode (string) -->
|
<!-- Set Journal private_passcode (string) -->
|
||||||
|
|||||||
@@ -2,72 +2,54 @@
|
|||||||
/**
|
/**
|
||||||
* @file ae_comp__modal_journal_export.svelte
|
* @file ae_comp__modal_journal_export.svelte
|
||||||
* @description Modal component for bulk exporting journal entries.
|
* @description Modal component for bulk exporting journal entries.
|
||||||
* Allows exporting the currently filtered list of entries to Markdown, HTML, or JSON.
|
* Allows exporting entries using various templates (Markdown, HTML, JSON).
|
||||||
* Supports both downloading as a file and copying to clipboard.
|
|
||||||
* @author One Sky IT
|
* @author One Sky IT
|
||||||
* @version 1.0.0
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Modal } from 'flowbite-svelte';
|
import { Modal } from 'flowbite-svelte';
|
||||||
import { Download, Copy, FileJson, FileType, Code } from '@lucide/svelte';
|
import { Download, Copy, FileJson, FileType, Code, Settings2 } from '@lucide/svelte';
|
||||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||||
import type { ae_JournalEntry } from '$lib/types/ae_types';
|
import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types';
|
||||||
|
import { EXPORT_TEMPLATES, type ExportTemplate } from '$lib/ae_journals/ae_journals_export_templates';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
entries: ae_JournalEntry[];
|
entries: ae_JournalEntry[];
|
||||||
|
journal?: ae_Journal;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { open = $bindable(false), entries = [], onClose }: Props = $props();
|
let { open = $bindable(false), entries = [], journal, onClose }: Props = $props();
|
||||||
|
|
||||||
// State
|
// State
|
||||||
let export_format: 'markdown' | 'html' | 'json' = $state('markdown');
|
let selected_template_id: string = $state('standard_markdown');
|
||||||
let export_preview: string = $state('');
|
let export_preview: string = $state('');
|
||||||
let export_count: number = $derived(entries.length);
|
let export_count: number = $derived(entries.length);
|
||||||
|
|
||||||
// Re-generate preview when entries or format changes
|
const templates = Object.values(EXPORT_TEMPLATES);
|
||||||
|
const selected_template = $derived(EXPORT_TEMPLATES[selected_template_id as keyof typeof EXPORT_TEMPLATES] || EXPORT_TEMPLATES.standard_markdown);
|
||||||
|
|
||||||
|
// Auto-select template based on journal type when modal opens
|
||||||
|
$effect(() => {
|
||||||
|
if (open && journal?.type_code) {
|
||||||
|
if (journal.type_code === 'personal_log' && EXPORT_TEMPLATES.personal_log) {
|
||||||
|
selected_template_id = 'personal_log';
|
||||||
|
} else if (journal.type_code === 'amazon_vine' && EXPORT_TEMPLATES.amazon_vine) {
|
||||||
|
selected_template_id = 'amazon_vine';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-generate preview when entries or template changes
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (open && entries.length > 0) {
|
if (open && entries.length > 0) {
|
||||||
generate_preview();
|
generate_preview();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates the export string based on the selected format.
|
|
||||||
*/
|
|
||||||
function generate_preview() {
|
function generate_preview() {
|
||||||
if (export_format === 'json') {
|
if (!selected_template) return;
|
||||||
// Simple JSON dump
|
export_preview = selected_template.formatter(entries);
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,10 +57,9 @@
|
|||||||
*/
|
*/
|
||||||
function handle_download() {
|
function handle_download() {
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
const filename = `journal_export_${timestamp}.${export_format === 'markdown' ? 'md' : export_format}`;
|
const filename = `journal_export_${timestamp}.${selected_template.extension}`;
|
||||||
const blob = new Blob([export_preview], { type: 'text/plain;charset=utf-8' });
|
const blob = new Blob([export_preview], { type: 'text/plain;charset=utf-8' });
|
||||||
|
|
||||||
// Create temporary link to trigger download
|
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
@@ -111,41 +92,48 @@
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
<div class="space-y-4">
|
<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">
|
<div class="flex items-center justify-between 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>
|
||||||
|
<strong>Ready to Export:</strong> {export_count} entries.
|
||||||
|
</div>
|
||||||
|
{#if journal}
|
||||||
|
<div class="text-xs opacity-70">
|
||||||
|
Journal Type: <span class="font-mono">{journal.type_code || 'standard'}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Format Selection -->
|
<!-- Template Selection -->
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="space-y-2">
|
||||||
<button
|
<label class="label flex items-center gap-2">
|
||||||
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' : ''}"
|
<Settings2 size="1em" />
|
||||||
onclick={() => { export_format = 'markdown'; generate_preview(); }}
|
<span class="label-text font-bold">Select Export Template</span>
|
||||||
>
|
</label>
|
||||||
<FileType size="2em" />
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-2">
|
||||||
<span class="font-bold">Markdown</span>
|
{#each templates as template}
|
||||||
</button>
|
<button
|
||||||
|
class="btn variant-ringed-surface flex flex-col gap-1 p-2 h-24 items-center justify-center border-2 text-center {selected_template_id === template.id ? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20' : 'opacity-60'}"
|
||||||
<button
|
onclick={() => { selected_template_id = template.id; generate_preview(); }}
|
||||||
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(); }}
|
{#if template.extension === 'json'}
|
||||||
>
|
<FileJson size="1.5em" />
|
||||||
<Code size="2em" />
|
{:else if template.extension === 'html'}
|
||||||
<span class="font-bold">HTML</span>
|
<Code size="1.5em" />
|
||||||
</button>
|
{:else}
|
||||||
|
<FileType size="1.5em" />
|
||||||
<button
|
{/if}
|
||||||
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' : ''}"
|
<span class="text-xs font-bold leading-tight">{template.name}</span>
|
||||||
onclick={() => { export_format = 'json'; generate_preview(); }}
|
<span class="text-[10px] opacity-70">.{template.extension}</span>
|
||||||
>
|
</button>
|
||||||
<FileJson size="2em" />
|
{/each}
|
||||||
<span class="font-bold">JSON</span>
|
</div>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Preview Area -->
|
<!-- Preview Area -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label flex justify-between items-center">
|
||||||
<span class="label-text">Preview</span>
|
<span class="label-text">Preview</span>
|
||||||
|
<span class="text-[10px] opacity-50 uppercase tracking-widest">{selected_template.name}</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
class="textarea h-64 font-mono text-xs bg-gray-50 dark:bg-gray-900"
|
class="textarea h-64 font-mono text-xs bg-gray-50 dark:bg-gray-900"
|
||||||
|
|||||||
Reference in New Issue
Block a user