feat(journals): modernize UI with responsive grid and fix Quick Add integration
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Aether Journals UI Update (2026)
|
# Aether Journals UI Update (2026)
|
||||||
|
|
||||||
> **Status:** Active
|
> **Status:** 🚧 Stuck on Phase 4 (Security/Encryption Blockers)
|
||||||
> **Last Updated:** 2026-01-14
|
> **Last Updated:** 2026-01-14
|
||||||
> **Primary Agent:** Frontend SvelteKit Agent
|
> **Primary Agent:** Frontend SvelteKit Agent
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ This document outlines the modernization of the Journals module UI in the Svelte
|
|||||||
2. **Quick Add UI:** Implement a specialized interface for rapid, friction-free entry creation.
|
2. **Quick Add UI:** Implement a specialized interface for rapid, friction-free entry creation.
|
||||||
3. **Append/Prepend UI:** Allow users to quickly add text to the beginning or end of existing entries without full edit mode.
|
3. **Append/Prepend UI:** Allow users to quickly add text to the beginning or end of existing entries without full edit mode.
|
||||||
4. **Interop & Portability:** Robust import/export logic for Markdown/HTML (Nextcloud Notes compatibility).
|
4. **Interop & Portability:** Robust import/export logic for Markdown/HTML (Nextcloud Notes compatibility).
|
||||||
5. **Security Hardening:** Review and harden client-side encryption logic.
|
5. **Security Hardening:** Review and harden client-side encryption logic (BLOCKED).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -39,31 +39,17 @@ This document outlines the modernization of the Journals module UI in the Svelte
|
|||||||
|
|
||||||
## 4. Feature Specifications
|
## 4. Feature Specifications
|
||||||
|
|
||||||
### ⚡ Quick Add
|
### ⚡ Quick Add (Complete)
|
||||||
* **Component:** `src/routes/journals/ae_comp__journal_entry_quick_add.svelte` (New)
|
* **Component:** `src/routes/journals/ae_comp__journal_entry_quick_add.svelte`
|
||||||
* **Placement:**
|
* **Behavior:** Creates a new `journal_entry` attached to the active journal without leaving the list view.
|
||||||
* Primary: Top of the Journal List view (`src/routes/journals/+page.svelte`).
|
|
||||||
* Secondary: Sidebar or global hotkey (Future).
|
|
||||||
* **Behavior:**
|
|
||||||
* Simple `textarea` input.
|
|
||||||
* "Add Note" button.
|
|
||||||
* **Logic:** Creates a new `journal_entry` attached to the active (or default) journal. Auto-sets `created_on` to `now`.
|
|
||||||
* **UX:** Should not require opening a full editor modal. Optimistic UI update.
|
|
||||||
|
|
||||||
### 📝 Append / Prepend
|
### 📝 Append / Prepend (Complete)
|
||||||
* **Triggers:** Buttons added to `src/routes/journals/JournalEntry_SettingsMenu.svelte`.
|
* **Interaction:** Fast text injection via `AeCompModalJournalEntryAppend`.
|
||||||
* **Interaction:**
|
* **Logic:** Updates entry content without full editor state overhead.
|
||||||
* Clicking "Append" or "Prepend" opens a focused input area (inline or small modal).
|
|
||||||
* User types text -> Submits.
|
|
||||||
* **Logic:**
|
|
||||||
* **Prepend:** `New Text` + `\n\n` + `Existing Content`
|
|
||||||
* **Append:** `Existing Content` + `\n\n` + `New Text`
|
|
||||||
* **UX:** seamless update of the view without entering full "Edit Mode".
|
|
||||||
|
|
||||||
### 🔄 Interop (Markdown/HTML)
|
### 🔄 Interop (Markdown/HTML) (Complete)
|
||||||
* **Goal:** Bulk export/import for data portability.
|
* **Goal:** Bulk export/import for data portability.
|
||||||
* **Format:** Optimized templates for different journal types (Standard, Personal Log, Amazon Vine).
|
* **Templates:** Standard, Personal Log, Amazon Vine.
|
||||||
* **Integration:** drag-and-drop zone for Nextcloud Note files and bulk parser logic in `ae_journals_parsers.ts`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -76,24 +62,38 @@ This document outlines the modernization of the Journals module UI in the Svelte
|
|||||||
### Phase 2: Rapid Entry (Complete)
|
### Phase 2: Rapid Entry (Complete)
|
||||||
- [x] Create `ae_comp__journal_entry_quick_add.svelte`.
|
- [x] Create `ae_comp__journal_entry_quick_add.svelte`.
|
||||||
- [x] Integrate Quick Add into `+page.svelte`.
|
- [x] Integrate Quick Add into `+page.svelte`.
|
||||||
- [x] Update `ae_journals_stores.ts` to manage Quick Add state/visibility.
|
|
||||||
|
|
||||||
### Phase 3: Content Manipulation & Portability (Complete)
|
### Phase 3: Content Manipulation & Portability (Complete)
|
||||||
- [x] Update `JournalEntry_SettingsMenu.svelte` with Append/Prepend actions.
|
- [x] Implement Append/Prepend logic.
|
||||||
- [x] Implement Append/Prepend logic in `ae_comp__journal_entry_obj_id_view.svelte`.
|
- [x] Implement Bulk Export/Import system.
|
||||||
- [x] Implement Bulk Export (Markdown/HTML/JSON) via `ae_comp__modal_journal_export.svelte`.
|
- [x] Establish centralized Export Template engine.
|
||||||
- [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 BLOCKER)
|
||||||
- [x] Implement Auto-Save toggle and visual status indicators.
|
- [x] Implement Auto-Save toggle and visual status indicators.
|
||||||
|
- [ ] Solidify E2EE passcode system for Journals and Entries.
|
||||||
|
- [ ] **BLOCKER:** Decryption workflow is currently unstable due to Svelte 5 reactivity loops.
|
||||||
- [ ] Audit encryption flow for Quick Added and Imported entries.
|
- [ ] Audit encryption flow for Quick Added and Imported entries.
|
||||||
- [x] Styling and Mobile responsiveness check for Import/Export modals.
|
|
||||||
- [ ] Integrate Outbound Email sharing.
|
- [ ] Integrate Outbound Email sharing.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Reference Files
|
## ⚠️ Technical Blocker: The "Decryption-Sync" Loop
|
||||||
* `src/lib/ae_journals/ae_journals_stores.ts`: State definitions.
|
|
||||||
* `src/routes/journals/+page.svelte`: Main UI container.
|
### The Issue
|
||||||
* `src/routes/journals/JournalEntry_Editor.svelte`: Editor logic.
|
The component is suffering from a **Reactive Feedback Loop** between decryption, auto-save, and background IDB refreshes.
|
||||||
|
1. **Decrypting content** triggers a change in `tmp_entry_obj.content`.
|
||||||
|
2. The **Auto-Save effect** sees this as a manual user edit and saves to the database.
|
||||||
|
3. **Dexie LiveQuery** detects the DB update and refreshes the object.
|
||||||
|
4. The **Sync effect** resets the entry to its encrypted state (from DB).
|
||||||
|
5. The **Auto-Decryption effect** fires again, starting the loop over.
|
||||||
|
|
||||||
|
### What we tried:
|
||||||
|
* **`is_processing` flags:** Attempted to block reactivity during decryption.
|
||||||
|
* **`untrack()`:** Attempted to isolate store updates.
|
||||||
|
* **Reference Sync:** Attempted to update `orig_entry_obj` simultaneously with decryption to fool the change detector.
|
||||||
|
* **Direct Store Updates:** Switching from property assignment to `journals_sess.update()` to fix Svelte 5 notification failures.
|
||||||
|
|
||||||
|
### Future Fix Ideas:
|
||||||
|
1. **Native Svelte 5 State:** Refactor `journals_sess` from a Svelte 4 `Writable` to a class using `$state`.
|
||||||
|
2. **Logic Extraction:** Move decryption logic into a non-reactive class/helper to isolate side effects from the UI render cycle.
|
||||||
|
3. **Hash Comparison:** Use content hashes for change detection instead of string comparisons to avoid whitespace/normalization loops.
|
||||||
|
|||||||
@@ -1,365 +1,213 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let log_lvl: number = 0;
|
/**
|
||||||
// console.log(`ae_journals +page data:`, data);
|
* src/routes/journals/+page.svelte
|
||||||
// console.log(`ae_journals Data Params:`, data.url.searchParams.get('journal_id'));
|
* Modernized Journals Index View
|
||||||
|
* Focus: Simplicity for regular users, power tools for edit mode.
|
||||||
// *** Import Svelte specific
|
*/
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
// *** Import other supporting libraries
|
// *** Icons
|
||||||
import { BookPlus, FolderPlus, Library, Loader, SquareLibrary, Wrench, FileUp, FileDown } from '@lucide/svelte';
|
import {
|
||||||
|
BookPlus, SquareLibrary, Wrench,
|
||||||
|
FileUp, Loader2, Sparkles
|
||||||
|
} from '@lucide/svelte';
|
||||||
|
|
||||||
|
// *** Libraries & Stores
|
||||||
import { liveQuery } from 'dexie';
|
import { liveQuery } from 'dexie';
|
||||||
import { Modal } from 'flowbite-svelte';
|
import { Modal } from 'flowbite-svelte';
|
||||||
|
|
||||||
// *** Import Aether specific variables and functions
|
|
||||||
// import { api } from '$lib/api';
|
|
||||||
import { db_journals } from '$lib/ae_journals/db_journals';
|
import { db_journals } from '$lib/ae_journals/db_journals';
|
||||||
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
|
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
|
||||||
import { ae_loc, ae_sess, ae_api, slct, slct_trigger } from '$lib/stores/ae_stores';
|
import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores';
|
||||||
import {
|
import {
|
||||||
journals_loc,
|
journals_loc,
|
||||||
journals_sess,
|
journals_sess,
|
||||||
journals_slct,
|
journals_slct,
|
||||||
journals_trig
|
journals_trig
|
||||||
} from '$lib/ae_journals/ae_journals_stores';
|
} from '$lib/ae_journals/ae_journals_stores';
|
||||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
|
||||||
|
// *** Components
|
||||||
|
import Modal_journals_cfg from './modal_journals_config.svelte';
|
||||||
|
import Journal_obj_li from './ae_comp__journal_obj_li.svelte';
|
||||||
|
import AeCompJournalEntryQuickAdd from './ae_comp__journal_entry_quick_add.svelte';
|
||||||
|
import AeCompModalJournalImport from './ae_comp__modal_journal_import.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: any;
|
data: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { data }: Props = $props();
|
let { data }: Props = $props();
|
||||||
|
|
||||||
import Modal_journals_cfg from './modal_journals_config.svelte';
|
// *** State
|
||||||
import Journal_obj_li from './ae_comp__journal_obj_li.svelte';
|
|
||||||
import AeCompJournalEntryQuickAdd from './ae_comp__journal_entry_quick_add.svelte';
|
|
||||||
import AeCompModalJournalImport from './ae_comp__modal_journal_import.svelte';
|
|
||||||
import AeCompModalJournalExport from './ae_comp__modal_journal_export.svelte';
|
|
||||||
|
|
||||||
// import Element_data_store from '$lib/element_data_store_v2.svelte';
|
|
||||||
|
|
||||||
let ae_acct = data[$slct.account_id];
|
|
||||||
let show_import_modal = $state(false);
|
let show_import_modal = $state(false);
|
||||||
let show_export_modal = $state(false);
|
let log_lvl = 0;
|
||||||
|
|
||||||
function handle_import_complete() {
|
|
||||||
// Trigger a refresh of the journal list?
|
|
||||||
// Usually liveQuery handles it if data changed in IDB.
|
|
||||||
// But if we just created entries, we might want to refresh the selected journal view if active.
|
|
||||||
// $journals_trig.journal_entry_li = true;
|
|
||||||
}
|
|
||||||
// $journals_slct.journal_obj = ae_acct.slct.journal_obj;
|
|
||||||
|
|
||||||
// We need to access the currently filtered/displayed list of entries for export.
|
|
||||||
// The list is actually managed inside 'Journal_obj_li' (the list component) or via a store.
|
|
||||||
// However, looking at Journal_obj_li, it takes 'lq__journal_entry_obj_li' as a prop.
|
|
||||||
// BUT wait, this page (+page.svelte) seems to be the Journal INDEX (list of journals),
|
|
||||||
// NOT the list of entries within a journal.
|
|
||||||
// Ah, checking the code...
|
|
||||||
// The main page lists *Journals* (lq__journal_obj_li).
|
|
||||||
// The entries are listed in [journal_id]/+page.svelte.
|
|
||||||
|
|
||||||
// Correction: Bulk Export should probably be on the [journal_id] page, OR here if we want to export ALL journals?
|
|
||||||
// The user request was "export Journal Entries". Usually implies from a specific journal or context.
|
|
||||||
// Let's check [journal_id]/+page.svelte.
|
|
||||||
|
|
||||||
// $journals_slct.journal_obj_li = ae_acct.slct.journal_obj_li;
|
|
||||||
|
|
||||||
|
// *** LiveQueries
|
||||||
let lq__journal_obj_li = $derived(
|
let lq__journal_obj_li = $derived(
|
||||||
liveQuery(async () => {
|
liveQuery(async () => {
|
||||||
let results = await db_journals.journal
|
return await db_journals.journal
|
||||||
.where('person_id')
|
.where('person_id')
|
||||||
.equals($ae_loc.person_id)
|
.equals($ae_loc.person_id)
|
||||||
// .sortBy('group')
|
|
||||||
// .sortBy('priority')
|
|
||||||
// .sortBy('sort')
|
|
||||||
.reverse()
|
.reverse()
|
||||||
.sortBy('tmp_sort_3');
|
.sortBy('tmp_sort_3');
|
||||||
|
|
||||||
// .orderBy('tmp_sort_3')
|
|
||||||
// .reverse()
|
|
||||||
// .toArray()
|
|
||||||
|
|
||||||
// .sortBy('start_datetime')
|
|
||||||
// () => db_journals.journals
|
|
||||||
// .where('conference')
|
|
||||||
// // .aboveOrEqual(0)
|
|
||||||
// .equals('true')
|
|
||||||
// // .above(0)
|
|
||||||
// .sortBy('name') // Use sortBy() instead of orderBy(). toArray() is also not needed???
|
|
||||||
// // .sortBy('[priority+name]')
|
|
||||||
// // .orderBy('name')
|
|
||||||
// // .offset(10).limit(5)
|
|
||||||
// // .toArray()
|
|
||||||
|
|
||||||
return results;
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
// console.log(`lq__journal_obj_li:`, $lq__journal_obj_li);
|
|
||||||
|
|
||||||
let lq__journal_obj = $derived(
|
|
||||||
liveQuery(async () => {
|
|
||||||
let results = await db_journals.journal
|
|
||||||
.where('id')
|
|
||||||
.equals(ae_acct.slct.journal_id)
|
|
||||||
.toArray();
|
|
||||||
// .first()
|
|
||||||
// .get($journals_slct.journal_id)
|
|
||||||
|
|
||||||
return results;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
console.log('Journals: +page.svelte');
|
|
||||||
|
|
||||||
console.log('ae_ slct:', $slct);
|
|
||||||
|
|
||||||
let href_url = window.location.href;
|
|
||||||
// console.log(href_url);
|
|
||||||
|
|
||||||
$ae_loc.href_url = href_url;
|
|
||||||
console.log(`lq__journal_obj = `, $lq__journal_obj);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function create_journal() {
|
async function create_journal() {
|
||||||
// Confirm before creating a new journal
|
if (!confirm('Create a new journal?')) return;
|
||||||
if (confirm('Are you sure you want to create a new journal?')) {
|
|
||||||
console.log('Creating new journal...');
|
|
||||||
} else {
|
|
||||||
console.log('Journal creation cancelled.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
const name = $journals_sess.journal.new_journal_name;
|
||||||
$journals_sess.journal.new_journal_name &&
|
const type = $journals_sess.journal.new_journal_type_code;
|
||||||
$journals_sess.journal.new_journal_type_code
|
|
||||||
) {
|
if (name && type) {
|
||||||
$journals_slct.journal_id = null;
|
|
||||||
try {
|
try {
|
||||||
let data_kv = {
|
const results = await journals_func.create_ae_obj__journal({
|
||||||
// account: $slct.account_id,
|
api_cfg: $ae_api,
|
||||||
person_id_random: $ae_loc.person_id,
|
account_id: $slct.account_id,
|
||||||
name: $journals_sess.journal.new_journal_name,
|
data_kv: {
|
||||||
type_code: $journals_sess.journal.new_journal_type_code,
|
person_id_random: $ae_loc.person_id,
|
||||||
cfg_json: {
|
name,
|
||||||
category_li: [{ code: '', name: 'Default' }]
|
type_code: type,
|
||||||
|
cfg_json: { category_li: [{ code: '', name: 'Default' }] }
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
journals_func
|
if (results?.journal_id_random) {
|
||||||
.create_ae_obj__journal({
|
$journals_sess.show__modal_new__journal_obj = false;
|
||||||
api_cfg: $ae_api,
|
goto(`/journals/${results.journal_id_random}`);
|
||||||
account_id: $slct.account_id,
|
}
|
||||||
data_kv: data_kv,
|
|
||||||
log_lvl: log_lvl
|
|
||||||
})
|
|
||||||
.then((results) => {
|
|
||||||
console.log('New journal created:', results);
|
|
||||||
$journals_slct.journal_id = results?.journal_id_random;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error creating journal:', error);
|
|
||||||
alert('Failed to create journal.');
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if ($journals_slct.journal_id) {
|
|
||||||
$journals_sess.show__modal_new__journal_obj = false;
|
|
||||||
goto(`/journals/${$journals_slct.journal_id}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// console.log('New journal created:', result);
|
|
||||||
// $journals_sess.show__modal_new__journal_obj = false;
|
|
||||||
// goto(`/journals/${result.journal_id_random}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating journal:', error);
|
|
||||||
alert('Failed to create journal.');
|
alert('Failed to create journal.');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert('Please provide both name and type for the journal.');
|
alert('Please provide a name and type.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Svelte Page for a AE Journals Module main page -->
|
<div class="page_container flex flex-col gap-8 items-center w-full min-h-screen p-4 md:p-8">
|
||||||
<section
|
|
||||||
class="
|
|
||||||
ae_journals
|
|
||||||
mx-auto
|
|
||||||
flex flex-col gap-1
|
|
||||||
items-center
|
|
||||||
min-h-full
|
|
||||||
max-h-max
|
|
||||||
min-w-full
|
|
||||||
max-w-max
|
|
||||||
space-y-2
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<h1 class="h1 text-4xl text-center text-gray-800 dark:text-gray-200">
|
|
||||||
<!-- <Library size="1em" class="mx-1 inline-block" /> -->
|
|
||||||
<SquareLibrary size="1em" class="mx-1 inline-block text-neutral-800/60" />
|
|
||||||
Journals for {$ae_loc.account_name ?? 'Æ loading...'}
|
|
||||||
{$ae_loc.person.given_name ? `- ${$ae_loc.person.given_name}` : ''}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<!-- Quick Add Section -->
|
<!-- Header Section -->
|
||||||
<div class="w-full max-w-2xl mx-auto px-4 pb-4">
|
<header class="text-center space-y-2 max-w-3xl">
|
||||||
<AeCompJournalEntryQuickAdd class="shadow-lg" />
|
<div class="flex items-center justify-center gap-3 mb-2">
|
||||||
</div>
|
<div class="p-3 bg-surface-500/10 rounded-2xl">
|
||||||
|
<SquareLibrary size="3em" class="text-primary-500" />
|
||||||
<div
|
</div>
|
||||||
class="flex flex-row flex-wrap gap-1 items-center justify-center w-full border-gray-200 border-y-2 py-2"
|
|
||||||
class:hidden={!$ae_loc.edit_mode}
|
|
||||||
>
|
|
||||||
<!-- Add new journal button -->
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="
|
|
||||||
btn btn-sm
|
|
||||||
preset-tonal-secondary border border-secondary-500
|
|
||||||
hover:preset-filled-secondary-500
|
|
||||||
transition
|
|
||||||
"
|
|
||||||
onclick={() => {
|
|
||||||
$journals_sess.show__modal_new__journal_obj = true;
|
|
||||||
}}
|
|
||||||
title="Create a new journal"
|
|
||||||
>
|
|
||||||
<!-- <FolderPlus class="mx-1" /> -->
|
|
||||||
<BookPlus class="mx-1" />
|
|
||||||
<span class="hidden md:inline"> New Journal </span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Import Entries button -->
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="
|
|
||||||
btn btn-sm
|
|
||||||
preset-tonal-secondary border border-secondary-500
|
|
||||||
hover:preset-filled-secondary-500
|
|
||||||
transition
|
|
||||||
"
|
|
||||||
onclick={() => {
|
|
||||||
show_import_modal = true;
|
|
||||||
}}
|
|
||||||
title="Import entries from Markdown files"
|
|
||||||
>
|
|
||||||
<FileUp class="mx-1" />
|
|
||||||
<span class="hidden md:inline"> Import </span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Show Journals Config button -->
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="
|
|
||||||
btn btn-sm
|
|
||||||
preset-tonal-secondary border border-secondary-500
|
|
||||||
hover:preset-filled-secondary-500
|
|
||||||
transition
|
|
||||||
"
|
|
||||||
onclick={() => {
|
|
||||||
$journals_sess.show__modal__journals_config = true;
|
|
||||||
// Redirect to the journals config page
|
|
||||||
// goto('/journals/config');
|
|
||||||
}}
|
|
||||||
title="Configure Journals"
|
|
||||||
>
|
|
||||||
<Wrench class="mx-1" />
|
|
||||||
<span class="hidden md:inline"> Journals Config </span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- <Element_data_store
|
|
||||||
ds_code="journals___overview"
|
|
||||||
ds_type="html"
|
|
||||||
for_type="journal"
|
|
||||||
for_id={$journals_slct.journal_id}
|
|
||||||
display="block"
|
|
||||||
class_li="p-2"
|
|
||||||
/> -->
|
|
||||||
|
|
||||||
<!-- <Element_data_store
|
|
||||||
ds_code="journals___example"
|
|
||||||
ds_type="html"
|
|
||||||
for_type="journal"
|
|
||||||
for_id={$journals_slct.journal_id}
|
|
||||||
ds_name="Default: Journals - Journals Example"
|
|
||||||
store="local"
|
|
||||||
display="block"
|
|
||||||
class_li="variant-ghost-surface p-2"
|
|
||||||
try_cache={true}
|
|
||||||
show_edit={false}
|
|
||||||
/> -->
|
|
||||||
|
|
||||||
{#await ae_acct.slct.journal_obj_li}
|
|
||||||
<div class="flex flex-col items-center justify-center p-8">
|
|
||||||
<Loader size="2em" class="animate-spin text-primary-500 mb-4" />
|
|
||||||
<span class="text-lg text-gray-600 dark:text-gray-400">Loading journals...</span>
|
|
||||||
</div>
|
</div>
|
||||||
{:then}
|
<h1 class="text-4xl md:text-5xl font-black tracking-tight text-surface-900 dark:text-surface-100">
|
||||||
<!-- <span class="flex flex-row items-center justify-center">
|
Journals
|
||||||
<span class="fas fa-check text-green-500 mx-1"></span>
|
</h1>
|
||||||
<span>Loaded</span>
|
<p class="text-surface-600 dark:text-surface-400 font-medium">
|
||||||
</span> -->
|
Managed by <span class="text-primary-500">{$ae_loc.account_name ?? 'Æ loading...'}</span>
|
||||||
|
{#if $ae_loc.person.given_name}
|
||||||
|
• <span class="opacity-75">{$ae_loc.person.given_name}</span>
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
{#if $lq__journal_obj_li && $lq__journal_obj_li?.length}
|
<!-- Quick Add Integrated Section -->
|
||||||
|
<section class="w-full max-w-2xl">
|
||||||
|
<div class="relative group">
|
||||||
|
<div class="absolute -inset-1 bg-gradient-to-r from-primary-500 to-secondary-500 rounded-2xl blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
|
||||||
|
<AeCompJournalEntryQuickAdd
|
||||||
|
journals_li={$lq__journal_obj_li}
|
||||||
|
class="relative shadow-2xl rounded-xl overflow-hidden border border-surface-500/10 bg-surface-50 dark:bg-surface-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 flex items-center justify-center gap-2 text-xs opacity-50 font-bold uppercase tracking-widest">
|
||||||
|
<Sparkles size="1em" />
|
||||||
|
<span>Fast Input Mode Active</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Administrative Action Bar (Edit Mode Only) -->
|
||||||
|
{#if $ae_loc.edit_mode}
|
||||||
|
<nav class="flex flex-row flex-wrap gap-3 items-center justify-center w-full py-4 border-y border-surface-500/10">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn variant-filled-secondary shadow-lg hover:scale-105 transition-transform"
|
||||||
|
onclick={() => $journals_sess.show__modal_new__journal_obj = true}
|
||||||
|
>
|
||||||
|
<BookPlus size="1.2em" class="mr-2" />
|
||||||
|
<span>New Journal</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn variant-filled-surface border border-surface-500/30 shadow-lg hover:scale-105 transition-transform"
|
||||||
|
onclick={() => show_import_modal = true}
|
||||||
|
>
|
||||||
|
<FileUp size="1.2em" class="mr-2" />
|
||||||
|
<span>Import</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn variant-soft-surface shadow-lg hover:scale-105 transition-transform"
|
||||||
|
onclick={() => $journals_sess.show__modal__journals_config = true}
|
||||||
|
>
|
||||||
|
<Wrench size="1.2em" class="mr-2" />
|
||||||
|
<span>Configure</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Main List Section -->
|
||||||
|
<main class="w-full flex justify-center">
|
||||||
|
{#if $lq__journal_obj_li === undefined}
|
||||||
|
<div class="flex flex-col items-center justify-center p-20 gap-4 opacity-50">
|
||||||
|
<Loader2 size="3em" class="animate-spin" />
|
||||||
|
<p class="text-xl font-bold">Accessing Brain...</p>
|
||||||
|
</div>
|
||||||
|
{:else if $lq__journal_obj_li.length > 0}
|
||||||
<Journal_obj_li {lq__journal_obj_li} />
|
<Journal_obj_li {lq__journal_obj_li} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col items-center justify-center p-4 text-center">
|
<div class="max-w-md text-center p-12 bg-surface-500/5 rounded-3xl border-2 border-dashed border-surface-500/20">
|
||||||
<p class="text-lg text-gray-600 dark:text-gray-400 mb-4">No journals found.</p>
|
<SquareLibrary size="4em" class="mx-auto mb-4 opacity-20" />
|
||||||
<p class="text-md text-gray-500 dark:text-gray-300">
|
<h3 class="text-2xl font-bold mb-2">No Journals Found</h3>
|
||||||
Click the "New Journal" button above to create your first journal.
|
<p class="opacity-60 mb-6">You haven't created any journals yet. Start by creating one to begin your documentation journey.</p>
|
||||||
</p>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn variant-filled-primary"
|
||||||
|
onclick={() => $journals_sess.show__modal_new__journal_obj = true}
|
||||||
|
>
|
||||||
|
Create Your First Journal
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:catch error}
|
</main>
|
||||||
<div class="text-red-800">
|
</div>
|
||||||
<span class="fas fa-exclamation-triangle text-xl"></span>
|
|
||||||
<span>Error: {error.message}</span>
|
|
||||||
</div>
|
|
||||||
{/await}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Modal for creating new journal -->
|
<!-- Modals -->
|
||||||
{#if $journals_sess.show__modal_new__journal_obj}
|
{#if $journals_sess.show__modal_new__journal_obj}
|
||||||
<Modal
|
<Modal
|
||||||
title="Create New Journal"
|
title="Create New Journal"
|
||||||
bind:open={$journals_sess.show__modal_new__journal_obj}
|
bind:open={$journals_sess.show__modal_new__journal_obj}
|
||||||
autoclose={false}
|
autoclose={false}
|
||||||
placement="top-center"
|
size="md"
|
||||||
size="xl"
|
class="bg-white dark:bg-surface-900 shadow-2xl rounded-2xl"
|
||||||
class="top-center bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md relative flex flex-col mx-auto w-full divide-y"
|
|
||||||
>
|
>
|
||||||
<div class="modal">
|
<div class="p-2 space-y-4">
|
||||||
<div class="modal-box">
|
<div class="space-y-1">
|
||||||
<h3 class="font-bold text-lg">Create New Journal</h3>
|
<label class="label text-sm font-bold opacity-75">Journal Name</label>
|
||||||
<div class="py-4">
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
placeholder="e.g. My Daily Logs"
|
||||||
placeholder="Journal Name"
|
bind:value={$journals_sess.journal.new_journal_name}
|
||||||
bind:value={$journals_sess.journal.new_journal_name}
|
class="input variant-filled-surface rounded-lg"
|
||||||
class="input input-bordered w-full mb-2"
|
/>
|
||||||
/>
|
</div>
|
||||||
<input
|
<div class="space-y-1">
|
||||||
type="text"
|
<label class="label text-sm font-bold opacity-75">Type Code</label>
|
||||||
placeholder="Journal Type"
|
<input
|
||||||
bind:value={$journals_sess.journal.new_journal_type_code}
|
type="text"
|
||||||
class="input input-bordered w-full mb-2"
|
placeholder="e.g. diary, log, notebook"
|
||||||
/>
|
bind:value={$journals_sess.journal.new_journal_type_code}
|
||||||
</div>
|
class="input variant-filled-surface rounded-lg"
|
||||||
<div class="modal-action">
|
/>
|
||||||
<button class="btn preset-tonal-primary" onclick={create_journal}>Create</button
|
</div>
|
||||||
>
|
<div class="flex justify-end gap-2 pt-4">
|
||||||
<button
|
<button type="button" class="btn variant-soft-surface" onclick={() => $journals_sess.show__modal_new__journal_obj = false}>Cancel</button>
|
||||||
type="button"
|
<button type="button" class="btn variant-filled-primary font-bold" onclick={create_journal}>Create Journal</button>
|
||||||
onclick={() => {
|
|
||||||
// Close the modal
|
|
||||||
$journals_sess.show__modal_new__journal_obj = false;
|
|
||||||
}}
|
|
||||||
class="btn preset-tonal-secondary"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -372,8 +220,5 @@
|
|||||||
<AeCompModalJournalImport
|
<AeCompModalJournalImport
|
||||||
bind:open={show_import_modal}
|
bind:open={show_import_modal}
|
||||||
onClose={() => (show_import_modal = false)}
|
onClose={() => (show_import_modal = false)}
|
||||||
onImportComplete={handle_import_complete}
|
onImportComplete={() => {}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<style lang="postcss">
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
// 1. Initial Load & Background Sync
|
// 1. Initial Load & Background Sync
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const entry = $lq__journal_entry_obj;
|
const entry = $lq__journal_entry_obj;
|
||||||
if (entry && entry.updated_on) {
|
if (entry && (entry.updated_on || entry.created_on)) {
|
||||||
// Only sync if saved and not currently processing/editing
|
// Only sync if saved and not currently processing/editing
|
||||||
if (save_status === 'saved' && !has_unsaved_changes && !is_processing) {
|
if (save_status === 'saved' && !has_unsaved_changes && !is_processing) {
|
||||||
const base = {
|
const base = {
|
||||||
|
|||||||
@@ -2,25 +2,31 @@
|
|||||||
import { api } from '$lib/api/api';
|
import { api } from '$lib/api/api';
|
||||||
import { ae_api } from '$lib/stores/ae_stores';
|
import { ae_api } from '$lib/stores/ae_stores';
|
||||||
import { journals_slct, journals_loc, journals_trig } from '$lib/ae_journals/ae_journals_stores';
|
import { journals_slct, journals_loc, journals_trig } from '$lib/ae_journals/ae_journals_stores';
|
||||||
|
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
|
||||||
|
import { BookType } from '@lucide/svelte';
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
let {
|
let {
|
||||||
class: className = "",
|
class: className = "",
|
||||||
placeholder = "Type your quick note... (First line = Title)"
|
placeholder = "Type your quick note... (First line = Title)",
|
||||||
|
journals_li = [] // Optional list of journals to select from
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
// State
|
// State
|
||||||
let note_content = $state("");
|
let note_content = $state("");
|
||||||
let is_submitting = $state(false);
|
let is_submitting = $state(false);
|
||||||
|
|
||||||
// Derived
|
// Derived / Local target
|
||||||
// Determine target journal: Selected > Default Preference > First Available?
|
// We prefer the persisted 'qry__journal_id' if we are on the main landing page
|
||||||
let target_journal_id = $derived($journals_slct.journal_id || $journals_loc.qry__journal_id);
|
let selected_journal_id = $state($journals_loc.qry__journal_id);
|
||||||
|
|
||||||
|
// If a journal is explicitly selected via slct (e.g. we are in a journal view), use that
|
||||||
|
let target_journal_id = $derived($journals_slct.journal_id || selected_journal_id);
|
||||||
|
|
||||||
async function handle_submit() {
|
async function handle_submit() {
|
||||||
if (!note_content.trim()) return;
|
if (!note_content.trim()) return;
|
||||||
if (!target_journal_id) {
|
if (!target_journal_id) {
|
||||||
alert("No journal selected!");
|
alert("Please select a target journal first.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,33 +36,32 @@
|
|||||||
let name = lines[0].substring(0, 100);
|
let name = lines[0].substring(0, 100);
|
||||||
if (lines[0].length > 100) name += "...";
|
if (lines[0].length > 100) name += "...";
|
||||||
|
|
||||||
// If content is just one line, name is content, content is content.
|
const data_kv = {
|
||||||
// If multiple lines, name is line 0, content is full text.
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
journal_id: target_journal_id,
|
|
||||||
name: name,
|
name: name,
|
||||||
content: note_content,
|
content: note_content,
|
||||||
type_code: 'note', // Default to note
|
type_code: 'note',
|
||||||
// created_on: handled by backend usually, or we can send ISO
|
private: false, // Ensure notes are public/decrypted by default
|
||||||
enabled: true,
|
enable: true,
|
||||||
hidden: false
|
hide: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use the store value directly via $ prefix
|
try {
|
||||||
const api_cfg = $ae_api;
|
const res = await journals_func.create_ae_obj__journal_entry({
|
||||||
|
api_cfg: $ae_api,
|
||||||
|
journal_id: target_journal_id, // Pass string ID here
|
||||||
|
data_kv: data_kv,
|
||||||
|
log_lvl: 1
|
||||||
|
});
|
||||||
|
|
||||||
const res = await api.create_ae_obj_v3({
|
if (res) {
|
||||||
api_cfg: api_cfg,
|
note_content = "";
|
||||||
obj_type: 'journal_entry',
|
// Trigger refresh
|
||||||
fields: payload
|
$journals_trig.journal_entry_li = true;
|
||||||
});
|
} else {
|
||||||
|
alert("Failed to create note.");
|
||||||
if (res) {
|
}
|
||||||
note_content = "";
|
} catch (error) {
|
||||||
// Trigger refresh
|
console.error("Error creating journal entry:", error);
|
||||||
$journals_trig.journal_entry_li = true;
|
|
||||||
} else {
|
|
||||||
alert("Failed to create note.");
|
alert("Failed to create note.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,18 +73,41 @@
|
|||||||
handle_submit();
|
handle_submit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handle_journal_change(e: Event) {
|
||||||
|
const val = (e.target as HTMLSelectElement).value;
|
||||||
|
selected_journal_id = val;
|
||||||
|
$journals_loc.qry__journal_id = val; // Persist choice
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card p-4 space-y-4 variant-filled-surface {className}">
|
<div class="card p-4 space-y-4 variant-filled-surface {className}">
|
||||||
<header class="flex justify-between items-center">
|
<header class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2">
|
||||||
<h3 class="h3">Quick Add</h3>
|
<h3 class="h3 flex items-center gap-2">
|
||||||
{#if !target_journal_id}
|
<BookType size="1.2em" class="text-primary-500" />
|
||||||
|
Quick Add
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{#if journals_li && journals_li.length > 0}
|
||||||
|
<div class="w-full sm:w-auto">
|
||||||
|
<select
|
||||||
|
class="select select-sm variant-filled-surface border-surface-500/30 font-bold"
|
||||||
|
value={target_journal_id}
|
||||||
|
onchange={handle_journal_change}
|
||||||
|
>
|
||||||
|
<option value="" disabled selected={!target_journal_id}>Select Target Journal...</option>
|
||||||
|
{#each journals_li as journal}
|
||||||
|
<option value={journal.id}>{journal.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{:else if !target_journal_id}
|
||||||
<span class="badge variant-filled-error">No Journal Selected</span>
|
<span class="badge variant-filled-error">No Journal Selected</span>
|
||||||
{/if}
|
{/if}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
class="textarea"
|
class="textarea variant-filled-surface border-surface-500/20"
|
||||||
rows="3"
|
rows="3"
|
||||||
bind:value={note_content}
|
bind:value={note_content}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
@@ -87,20 +115,25 @@
|
|||||||
disabled={is_submitting}
|
disabled={is_submitting}
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<div class="flex justify-end space-x-2">
|
<div class="flex justify-between items-center">
|
||||||
<button
|
<span class="text-[10px] opacity-50 font-mono uppercase tracking-tighter hidden sm:block">
|
||||||
class="btn variant-ghost-surface"
|
Press Ctrl + Enter to save
|
||||||
onclick={() => note_content = ""}
|
</span>
|
||||||
disabled={is_submitting || note_content.length === 0}
|
<div class="flex justify-end space-x-2 grow sm:grow-0">
|
||||||
>
|
<button
|
||||||
Clear
|
class="btn btn-sm variant-ghost-surface"
|
||||||
</button>
|
onclick={() => note_content = ""}
|
||||||
<button
|
disabled={is_submitting || note_content.length === 0}
|
||||||
class="btn variant-filled-primary"
|
>
|
||||||
onclick={handle_submit}
|
Clear
|
||||||
disabled={is_submitting || !target_journal_id || note_content.length === 0}
|
</button>
|
||||||
>
|
<button
|
||||||
{#if is_submitting}Saving...{:else}Add Note{/if}
|
class="btn btn-sm variant-filled-primary font-bold shadow-md"
|
||||||
</button>
|
onclick={handle_submit}
|
||||||
|
disabled={is_submitting || !target_journal_id || note_content.length === 0}
|
||||||
|
>
|
||||||
|
{#if is_submitting}Saving...{:else}Add Note{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,31 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// let log_lvl: number = 0;
|
/**
|
||||||
|
* ae_comp__journal_obj_li.svelte
|
||||||
// *** Import Svelte specific
|
* Modernized Journal List Component
|
||||||
// import { goto } from '$app/navigation';
|
* Layout: Responsive Grid (1 col mobile, 2 col tablet, 3 col desktop)
|
||||||
|
* Style: Tailwind 4 + Skeleton UI Reference Standard
|
||||||
// *** Import other supporting libraries
|
*/
|
||||||
// import { Spinner } from 'flowbite-svelte';
|
import { BookOpenText, BookType, Hash, Calendar, Clock } from '@lucide/svelte';
|
||||||
import { BookOpenText, BookType } from '@lucide/svelte';
|
|
||||||
|
|
||||||
// *** Import Aether specific variables and functions
|
|
||||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||||
import {
|
import { ae_loc } from '$lib/stores/ae_stores';
|
||||||
ae_snip,
|
|
||||||
ae_loc,
|
|
||||||
ae_sess,
|
|
||||||
ae_api,
|
|
||||||
ae_trig,
|
|
||||||
slct,
|
|
||||||
slct_trigger
|
|
||||||
} from '$lib/stores/ae_stores';
|
|
||||||
import {
|
|
||||||
journals_loc,
|
|
||||||
journals_sess,
|
|
||||||
journals_slct
|
|
||||||
} from '$lib/ae_journals/ae_journals_stores';
|
|
||||||
|
|
||||||
// *** Setup Svelte properties
|
|
||||||
interface Props {
|
interface Props {
|
||||||
lq__journal_obj_li: any;
|
lq__journal_obj_li: any;
|
||||||
}
|
}
|
||||||
@@ -33,128 +16,99 @@
|
|||||||
let { lq__journal_obj_li }: Props = $props();
|
let { lq__journal_obj_li }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="journal_list flex flex-col gap-2 items-center justify-center w-full">
|
<!-- Responsive Grid Container -->
|
||||||
|
<section class="journal_list grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full max-w-7xl px-4">
|
||||||
{#if $lq__journal_obj_li && $lq__journal_obj_li.length}
|
{#if $lq__journal_obj_li && $lq__journal_obj_li.length}
|
||||||
{#each $lq__journal_obj_li as journals_journal_obj, index}
|
{#each $lq__journal_obj_li as journal, index}
|
||||||
<div
|
<div
|
||||||
class="
|
class="
|
||||||
container journal journal_obj
|
journal_card
|
||||||
border border-surface-500/30 rounded-lg p-3 mb-4 space-y-3
|
group relative
|
||||||
w-full max-w-2xl
|
flex flex-col justify-between
|
||||||
flex flex-col items-center justify-center
|
bg-surface-50 dark:bg-surface-900
|
||||||
bg-surface-50 dark:bg-surface-900
|
border border-surface-500/20 rounded-xl
|
||||||
shadow-sm hover:shadow-md transition-shadow
|
p-5 shadow-sm hover:shadow-xl hover:border-primary-500/50
|
||||||
"
|
transition-all duration-200 ease-in-out
|
||||||
class:hidden={(journals_journal_obj?.hide || !journals_journal_obj?.enable) &&
|
"
|
||||||
!$ae_loc.trusted_access}
|
class:hidden={(journal?.hide || !journal?.enable) && !$ae_loc.trusted_access}
|
||||||
class:opacity-50={journals_journal_obj.hide}
|
class:opacity-60={journal.hide}
|
||||||
class:preset-filled-warning-100-900={!journals_journal_obj?.enable}
|
class:border-warning-500={!journal?.enable}
|
||||||
>
|
>
|
||||||
<header
|
<!-- Top Section: Title & Badge -->
|
||||||
class="
|
<div class="space-y-3">
|
||||||
ae_header
|
<header class="flex justify-between items-start gap-2">
|
||||||
flex flex-row gap-2 items-center justify-between
|
<div class="flex items-center gap-3">
|
||||||
w-full
|
<div class="p-2 bg-primary-500/10 rounded-lg text-primary-500 group-hover:bg-primary-500 group-hover:text-white transition-colors">
|
||||||
"
|
<BookType size="1.5em" />
|
||||||
>
|
</div>
|
||||||
<h3 class="journal__name text-xl md:text-2xl font-bold flex items-center gap-2 text-surface-900 dark:text-surface-100">
|
<h3 class="text-xl font-bold text-surface-900 dark:text-surface-100 line-clamp-1">
|
||||||
<BookType size="1.25em" class="text-primary-500" />
|
{journal.name}
|
||||||
<span class="journal__name truncate">{journals_journal_obj.name}</span>
|
</h3>
|
||||||
</h3>
|
</div>
|
||||||
|
|
||||||
<!-- Show a label if the type code is set -->
|
{#if journal.type_code}
|
||||||
{#if journals_journal_obj.type_code}
|
<span class="badge preset-tonal-warning text-[10px] uppercase tracking-wider font-bold">
|
||||||
<span
|
{journal.type_code}
|
||||||
class="
|
|
||||||
preset-tonal-warning
|
|
||||||
text-xs font-semibold
|
|
||||||
mr-2 px-2.5 py-0.5
|
|
||||||
rounded
|
|
||||||
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<!-- <span class="ae_label">Type:</span> -->
|
|
||||||
<span class="ae_value">{journals_journal_obj.type_code}</span>
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- {journals_journal_obj?.tmp_sort_3} -->
|
|
||||||
|
|
||||||
{#if journals_journal_obj.description && $ae_loc.edit_mode}
|
|
||||||
<div
|
|
||||||
class="
|
|
||||||
prose
|
|
||||||
space-y-1
|
|
||||||
journal__description
|
|
||||||
p-2
|
|
||||||
w-full max-w-(--breakpoint-sm) md:max-w-(--breakpoint-md)
|
|
||||||
font-mono
|
|
||||||
text-gray-900
|
|
||||||
dark:bg-blue-900 dark:text-gray-100
|
|
||||||
shadow-md rounded-lg
|
|
||||||
text-sm font-normal text-wrap word-break
|
|
||||||
|
|
||||||
prose-h1:underline prose-h1:decoration-double
|
|
||||||
prose-h2:underline
|
|
||||||
prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg
|
|
||||||
prose-h1:m-0 prose-h2:m-0 prose-h3:m-0 prose-h4:m-0 prose-h5:m-0 prose-h6:m-0
|
|
||||||
prose-li:m-0 prose-li:p-0 prose-li:line-height-none
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{@html journals_journal_obj.description_md_html}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="ae_options flex flex-row flex-wrap gap-3 items-center justify-center">
|
|
||||||
<a
|
|
||||||
href="/journals/{journals_journal_obj?.journal_id}"
|
|
||||||
class="btn variant-filled-primary md:btn-lg font-bold shadow-lg hover:scale-105 transition-transform"
|
|
||||||
title={`View: ${journals_journal_obj?.name}`}
|
|
||||||
>
|
|
||||||
<BookOpenText size="1.25em" />
|
|
||||||
<span>Open Journal</span>
|
|
||||||
|
|
||||||
{#if journals_journal_obj?.journal_entry_count}
|
|
||||||
<span class="badge variant-filled-secondary ml-2">
|
|
||||||
{journals_journal_obj?.journal_entry_count} {journals_journal_obj?.journal_entry_count === 1 ? 'entry' : 'entries'}
|
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</header>
|
||||||
|
|
||||||
|
<!-- Description (Power User / Edit Mode Only) -->
|
||||||
|
{#if journal.description && $ae_loc.edit_mode}
|
||||||
|
<div class="prose prose-sm dark:prose-invert max-h-24 overflow-y-auto bg-surface-100/50 dark:bg-surface-800/50 p-3 rounded-lg text-xs font-mono">
|
||||||
|
{@html journal.description_md_html}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Quick Stats (Clean View) -->
|
||||||
|
<div class="flex items-center gap-4 text-xs text-surface-600 dark:text-surface-400">
|
||||||
|
<div class="flex items-center gap-1" title="Entry Count">
|
||||||
|
<Hash size="1.2em" />
|
||||||
|
<span class="font-bold">{journal.journal_entry_count ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1" title="Last Updated">
|
||||||
|
<Clock size="1.2em" />
|
||||||
|
<span>{ae_util.iso_datetime_formatter(journal.updated_on || journal.created_on, 'date_short')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section
|
<!-- Bottom Section: Actions -->
|
||||||
class="ae_section ae_footer ae_meta journal__meta mt-2 flex flex-col sm:flex-row gap-1 items-center justify-center text-sm text-gray-500"
|
<div class="mt-6 pt-4 border-t border-surface-500/10 flex flex-col gap-3">
|
||||||
class:hidden={!$ae_loc.administrator_access || !$ae_loc.edit_mode}
|
<a
|
||||||
>
|
href="/journals/{journal?.journal_id}"
|
||||||
<div class="ae_group">
|
class="btn variant-filled-primary w-full font-bold shadow-md hover:scale-[1.02] active:scale-95 transition-all"
|
||||||
{#if !journals_journal_obj.updated_on}
|
>
|
||||||
<span class="journal__created_on">
|
<BookOpenText size="1.2em" class="mr-2" />
|
||||||
<span class="ae_label">Created on:</span>
|
<span>Open Journal</span>
|
||||||
<span class="ae_value"
|
</a>
|
||||||
>{ae_util.iso_datetime_formatter(
|
|
||||||
journals_journal_obj.created_on,
|
<!-- Admin Metadata (Edit Mode Only) -->
|
||||||
'datetime_12_long'
|
{#if $ae_loc.edit_mode && $ae_loc.administrator_access}
|
||||||
)}</span
|
<div class="flex items-center justify-center gap-2 text-[10px] opacity-50 font-mono">
|
||||||
>
|
<Calendar size="1em" />
|
||||||
</span>
|
<span>Created: {ae_util.iso_datetime_formatter(journal.created_on, 'datetime_iso_12_no_seconds')}</span>
|
||||||
{:else}
|
</div>
|
||||||
<span class="journal__updated_on">
|
{/if}
|
||||||
<span class="ae_label">Updated on:</span>
|
</div>
|
||||||
<span class="ae_value"
|
|
||||||
>{ae_util.iso_datetime_formatter(
|
<!-- Status Indicators (Edit Mode Only) -->
|
||||||
journals_journal_obj.updated_on,
|
{#if $ae_loc.edit_mode}
|
||||||
'datetime_12_long'
|
<div class="absolute -top-2 -right-2 flex gap-1">
|
||||||
)}</span
|
{#if journal.hide}
|
||||||
>
|
<span class="badge-icon variant-filled-surface shadow-sm" title="Hidden">🚫</span>
|
||||||
</span>
|
{/if}
|
||||||
|
{#if !journal.enable}
|
||||||
|
<span class="badge-icon variant-filled-warning shadow-sm" title="Disabled">⚠️</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
No journals found at this time
|
<div class="col-span-full p-20 text-center opacity-50 flex flex-col items-center gap-4">
|
||||||
|
<BookType size="4em" />
|
||||||
|
<p class="text-xl">No journals found in this view.</p>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
<!-- {/if} -->
|
|
||||||
|
|||||||
Reference in New Issue
Block a user