Files
OSIT-AE-App-Svelte/src/routes/journals/ae_comp__modal_journal_import.svelte
Scott Idem b543c8a930 chore: migrate all FA icons to Lucide (@lucide/svelte)
- Replaced all active FontAwesome <span class="fas fa-*"> icons with
  Lucide components across 145 files (excluding /idaa/ which is intentional)
- Fixed merge script bug: consolidated lucide-svelte imports into @lucide/svelte
- Replaced dynamic toggle patterns (fa-toggle-on/off) with ToggleRight/ToggleLeft
- Replaced fa-eye/fa-eye-slash with Eye/EyeOff
- Replaced fa-bug/fa-bug-slash with Bug/BugOff
- Replaced fa-sync fa-spin with RefreshCw + animate-spin
- Replaced fa-microchip with Cpu
- Fixed {@const} placement in element_manage_event_file_li.svelte
- Removed obsolete CSS hover rules for .unlock_icon/.lock_icon
- svelte-check: 0 errors, 0 warnings
2026-03-16 18:07:43 -04:00

289 lines
9.6 KiB
Svelte

<script lang="ts">
import { Modal } from 'flowbite-svelte';
import { Check, CircleAlert, FileText, RefreshCw, Upload, X } from '@lucide/svelte';
import {
PARSERS,
type AeJournalEntryInput
} from '$lib/ae_journals/ae_journals_parsers';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import { ae_api } from '$lib/stores/ae_stores';
import { journals_slct } from '$lib/ae_journals/ae_journals_stores';
interface Props {
open: boolean;
on_close: () => void;
on_import_complete: () => void;
}
let {
open = $bindable(false),
on_close,
on_import_complete
}: Props = $props();
let files: FileList | null = $state(null);
let selected_parser: keyof typeof PARSERS = $state('standard');
let parsed_entries: AeJournalEntryInput[] = $state([]);
let is_parsing = $state(false);
let is_importing = $state(false);
let import_log: string[] = $state([]);
let is_dragging = $state(false);
// Watch for file selection or parser change to trigger parsing
$effect(() => {
if (files && files.length > 0) {
parse_files();
}
});
function handle_drag_enter(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
is_dragging = true;
}
function handle_drag_leave(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
is_dragging = false;
}
function handle_drag_over(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
is_dragging = true;
}
function handle_drop(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
is_dragging = false;
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
files = e.dataTransfer.files;
}
}
async function parse_files() {
if (!files) return;
is_parsing = true;
parsed_entries = [];
const parser = PARSERS[selected_parser];
for (let i = 0; i < files.length; i++) {
const file = files[i];
try {
const text = await file.text();
const entries = await parser(file, text);
parsed_entries = [...parsed_entries, ...entries];
} catch (err) {
console.error(`Error parsing ${file.name}:`, err);
}
}
is_parsing = false;
}
async function handle_import() {
if (parsed_entries.length === 0) return;
is_importing = true;
import_log = [];
const journal_id = $journals_slct.journal_id;
if (!journal_id) {
alert(
'No target journal selected. Please select a journal in the background first.'
);
is_importing = false;
return;
}
let success_count = 0;
for (const entry of parsed_entries) {
try {
// Construct payload
const data_kv = {
name: entry.name,
content: entry.content,
tags: entry.tags.join(', '),
type_code: entry.type_code || 'note',
created_on: entry.created_on,
updated_on: entry.updated_on
};
const res = await journals_func.create_ae_obj__journal_entry({
api_cfg: $ae_api,
journal_id: journal_id,
data_kv: data_kv,
log_lvl: 0
});
if (res) {
import_log.push(`✅ Imported: ${entry.name}`);
success_count++;
} else {
import_log.push(`❌ Failed: ${entry.name}`);
}
} catch (err) {
import_log.push(`❌ Error: ${entry.name} - ${err}`);
}
}
is_importing = false;
alert(
`Import complete! ${success_count}/${parsed_entries.length} imported.`
);
on_import_complete();
open = false;
}
</script>
<Modal
title="Import Journal Entries"
bind:open
autoclose={false}
size="xl"
class="w-full"
>
<div class="space-y-4">
<!-- Configuration -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="label">
<span>Parser Strategy</span>
<select class="select" bind:value={selected_parser}>
<option value="standard"
>Standard (1 File = 1 Entry)</option
>
<option value="personal_log"
>Personal Log (Split by Date)</option
>
<option value="amazon_vine">Amazon Vine Reviews</option>
</select>
</label>
</div>
<div>
<div class="label mb-2">
<span>Select Files</span>
</div>
<!-- Drop Zone -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="
border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-all duration-200
flex flex-col items-center justify-center gap-2
{is_dragging
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'}
"
ondragenter={handle_drag_enter}
ondragleave={handle_drag_leave}
ondragover={handle_drag_over}
ondrop={handle_drop}
onclick={() =>
document.getElementById('file_import_input')?.click()}
>
<Upload class="h-10 w-10 text-gray-400" />
<p class="text-sm text-gray-500">
<span
class="font-semibold text-primary-600 hover:text-primary-500"
>Click to upload</span
>
or drag and drop
</p>
<p class="text-xs text-gray-400">
Markdown (.md) or Text (.txt) files
</p>
<input
id="file_import_input"
type="file"
class="hidden"
multiple
accept=".md,.txt"
onchange={(e) => (files = e.currentTarget.files)}
/>
</div>
</div>
</div>
<!-- Preview -->
<div
class="border rounded-lg p-2 bg-gray-50 dark:bg-gray-900 max-h-64 overflow-y-auto"
>
<h4 class="font-bold mb-2 flex justify-between">
<span>Preview ({parsed_entries.length} entries)</span>
{#if is_parsing}
<RefreshCw class="animate-spin" />
{/if}
</h4>
{#if parsed_entries.length > 0}
<table class="table table-compact w-full text-xs">
<thead>
<tr>
<th>Name</th>
<th>Date</th>
<th>Tags</th>
</tr>
</thead>
<tbody>
{#each parsed_entries as entry, i (i)}
<tr>
<td
class="truncate max-w-[200px]"
title={entry.name}>{entry.name}</td
>
<td>{entry.created_on?.substring(0, 10)}</td>
<td>{entry.tags.join(', ')}</td>
</tr>
{/each}
</tbody>
</table>
{:else if files && files.length > 0 && !is_parsing}
<div class="text-center text-gray-500 py-4">
No entries found in selected files.
</div>
{:else if !files}
<div class="text-center text-gray-500 py-4">
Select files to preview import.
</div>
{/if}
</div>
<!-- Import Log -->
{#if import_log.length > 0}
<div
class="bg-black text-green-400 p-2 rounded text-xs font-mono max-h-32 overflow-y-auto"
>
{#each import_log as log, i (i)}
<div>{log}</div>
{/each}
</div>
{/if}
<div class="modal-action">
<button
type="button"
class="btn preset-tonal-secondary"
onclick={on_close}>Cancel</button
>
<button
type="button"
class="btn preset-filled-primary"
disabled={parsed_entries.length === 0 || is_importing}
onclick={handle_import}
>
{#if is_importing}
Importing...
{:else}
Import {parsed_entries.length} Entries
{/if}
</button>
</div>
</div>
</Modal>