Jacob Schmidt 54bef33f0b Refactor frontend state to store-first architecture
- add and expand feature stores for entries, fragments, todos, lists, settings

- move CRUD logic into store helpers and simplify component state handling

- update SidePanel + button to create section-specific items

- switch fragment UX to view-first with edit/create modes

- add and update docs for store-based state management

- remove deprecated account route
2026-02-25 20:52:46 -06:00

433 lines
11 KiB
Svelte

<script lang="ts">
import { goto } from "$app/navigation";
import AppModal from "$lib/components/AppModal.svelte";
import Navbar from "$lib/components/Navbar.svelte";
import {
addFragmentType,
addSettingsTag,
removeFragmentType,
removeSettingsTag,
settingsFragmentTypes,
settingsTags,
updateFragmentType,
updateSettingsTag
} from "$lib/stores/settings";
const activeSection = "settings";
let modalOpen = false;
let modalTitle = "";
let modalMessage = "";
let modalConfirmText = "OK";
let modalCancelText = "Cancel";
let modalShowCancel = false;
let modalTone: "default" | "danger" = "default";
let modalAction: "logout-confirm" | "logout-info" | null = null;
let newTag = "";
let newFragmentType = "";
let editingTagIndex: number | null = null;
let editingTagValue = "";
let editingFragmentTypeIndex: number | null = null;
let editingFragmentTypeValue = "";
function showModal(options: {
action: "logout-confirm" | "logout-info";
title: string;
message: string;
confirmText?: string;
cancelText?: string;
showCancel?: boolean;
tone?: "default" | "danger";
}) {
modalAction = options.action;
modalTitle = options.title;
modalMessage = options.message;
modalConfirmText = options.confirmText ?? "OK";
modalCancelText = options.cancelText ?? "Cancel";
modalShowCancel = options.showCancel ?? false;
modalTone = options.tone ?? "default";
modalOpen = true;
}
function closeModal() {
modalOpen = false;
modalAction = null;
}
function handleModalConfirm() {
if (modalAction === "logout-confirm") {
showModal({
action: "logout-info",
title: "Logout Requested",
message: "You have been logged out.",
confirmText: "Close"
});
return;
}
closeModal();
}
function handleSelect(id: string) {
if (id === "logout") {
showModal({
action: "logout-confirm",
title: "Confirm Logout",
message: "Are you sure you want to log out?",
confirmText: "Log Out",
cancelText: "Cancel",
showCancel: true,
tone: "danger"
});
return;
}
if (id === "settings") {
return;
}
if (id === "account") {
goto("/account");
return;
}
goto("/");
}
function addTag() {
if (addSettingsTag(newTag)) {
newTag = "";
}
}
function startEditTag(index: number, tag: string) {
editingTagIndex = index;
editingTagValue = tag;
}
function saveEditTag() {
if (editingTagIndex === null) return;
if (updateSettingsTag(editingTagIndex, editingTagValue)) {
editingTagIndex = null;
editingTagValue = "";
}
}
function cancelEditTag() {
editingTagIndex = null;
editingTagValue = "";
}
function removeTag(index: number) {
if (!removeSettingsTag(index)) return;
if (editingTagIndex === index) {
cancelEditTag();
}
}
function addFragmentTypeLocal() {
if (addFragmentType(newFragmentType)) {
newFragmentType = "";
}
}
function startEditFragmentType(index: number, fragmentType: string) {
editingFragmentTypeIndex = index;
editingFragmentTypeValue = fragmentType;
}
function saveEditFragmentType() {
if (editingFragmentTypeIndex === null) return;
if (updateFragmentType(editingFragmentTypeIndex, editingFragmentTypeValue)) {
editingFragmentTypeIndex = null;
editingFragmentTypeValue = "";
}
}
function cancelEditFragmentType() {
editingFragmentTypeIndex = null;
editingFragmentTypeValue = "";
}
function removeFragmentTypeLocal(index: number) {
if (!removeFragmentType(index)) return;
if (editingFragmentTypeIndex === index) {
cancelEditFragmentType();
}
}
</script>
<div class="app-shell panel-closed">
<Navbar {activeSection} onSelect={handleSelect} />
<main class="route-view">
<header class="route-header">
<h1>Settings</h1>
<p>Configure app behavior and interface options.</p>
</header>
<section class="route-card">
<label class="toggle-row">
<input type="checkbox" checked />
<span>Launch to last opened entry</span>
</label>
<label class="toggle-row">
<input type="checkbox" />
<span>Enable compact editor mode</span>
</label>
<label>
Default startup view
<select>
<option>Entries</option>
<option>Calendar</option>
<option>Fragments</option>
</select>
</label>
</section>
<section class="route-card">
<h2>Tags</h2>
<p class="section-copy">Add and manage tags used for notes and entries.</p>
<div class="create-row">
<input
type="text"
placeholder="Add tag (example: Research)"
bind:value={newTag}
on:keydown={(event) => event.key === "Enter" && addTag()}
/>
<button type="button" class="secondary-btn" on:click={addTag}>Add</button>
</div>
<ul class="item-list">
{#each $settingsTags as tag, index}
<li class="item-row">
{#if editingTagIndex === index}
<input
type="text"
bind:value={editingTagValue}
on:keydown={(event) => {
if (event.key === "Enter") saveEditTag();
if (event.key === "Escape") cancelEditTag();
}}
/>
<div class="row-actions">
<button type="button" class="secondary-btn" on:click={saveEditTag}>Save</button>
<button type="button" class="ghost-btn" on:click={cancelEditTag}>Cancel</button>
</div>
{:else}
<span>{tag}</span>
<div class="row-actions">
<button type="button" class="ghost-btn" on:click={() => startEditTag(index, tag)}>Edit</button>
<button type="button" class="danger-btn" on:click={() => removeTag(index)}>Remove</button>
</div>
{/if}
</li>
{/each}
</ul>
</section>
<section class="route-card">
<h2>Fragment Types</h2>
<p class="section-copy">Configure custom fragment types for the Fragments section.</p>
<div class="create-row">
<input
type="text"
placeholder="Add fragment type (example: Observation)"
bind:value={newFragmentType}
on:keydown={(event) => event.key === "Enter" && addFragmentTypeLocal()}
/>
<button type="button" class="secondary-btn" on:click={addFragmentTypeLocal}>Add</button>
</div>
<ul class="item-list">
{#each $settingsFragmentTypes as type, index}
<li class="item-row">
{#if editingFragmentTypeIndex === index}
<input
type="text"
bind:value={editingFragmentTypeValue}
on:keydown={(event) => {
if (event.key === "Enter") saveEditFragmentType();
if (event.key === "Escape") cancelEditFragmentType();
}}
/>
<div class="row-actions">
<button type="button" class="secondary-btn" on:click={saveEditFragmentType}>Save</button>
<button type="button" class="ghost-btn" on:click={cancelEditFragmentType}>Cancel</button>
</div>
{:else}
<span>{type}</span>
<div class="row-actions">
<button type="button" class="ghost-btn" on:click={() => startEditFragmentType(index, type)}>Edit</button>
<button type="button" class="danger-btn" on:click={() => removeFragmentTypeLocal(index)}>Remove</button>
</div>
{/if}
</li>
{/each}
</ul>
</section>
</main>
</div>
<AppModal
open={modalOpen}
title={modalTitle}
message={modalMessage}
confirmText={modalConfirmText}
cancelText={modalCancelText}
showCancel={modalShowCancel}
tone={modalTone}
onConfirm={handleModalConfirm}
onCancel={closeModal}
/>
<style>
.route-view {
min-height: 100vh;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
background: linear-gradient(180deg, var(--surface-2) 0%, var(--bg-editor) 100%);
color: var(--text-primary);
}
.route-header h1 {
font-size: 1.1rem;
margin-bottom: 4px;
}
.route-header p {
color: var(--text-muted);
font-size: 0.9rem;
}
.route-card {
border: 1px solid var(--border-soft);
background: var(--surface-1);
border-radius: 10px;
padding: 14px;
display: flex;
flex-direction: column;
gap: 12px;
max-width: 640px;
}
.toggle-row {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-muted);
font-size: 0.88rem;
}
.route-card label {
display: flex;
flex-direction: column;
gap: 5px;
color: var(--text-muted);
font-size: 0.82rem;
}
.route-card select {
border: 1px solid var(--border-soft);
border-radius: 7px;
background: var(--bg-app);
color: var(--text-primary);
padding: 8px 10px;
}
.route-card h2 {
font-size: 0.95rem;
color: var(--text-primary);
}
.section-copy {
font-size: 0.82rem;
color: var(--text-muted);
}
.create-row {
display: flex;
gap: 8px;
}
.create-row input {
flex: 1;
}
.item-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.item-row {
border: 1px solid var(--border-soft);
border-radius: 8px;
padding: 8px 10px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
font-size: 0.84rem;
color: var(--text-primary);
background: var(--bg-app);
}
.item-row input,
.route-card input {
border: 1px solid var(--border-soft);
border-radius: 7px;
background: var(--bg-app);
color: var(--text-primary);
padding: 6px 9px;
min-width: 200px;
}
.row-actions {
display: flex;
gap: 6px;
}
.secondary-btn,
.ghost-btn,
.danger-btn {
border: 1px solid var(--border-soft);
border-radius: 7px;
padding: 6px 10px;
font-size: 0.78rem;
cursor: pointer;
color: var(--text-primary);
}
.secondary-btn {
background: var(--surface-3);
border-color: var(--border-strong);
}
.ghost-btn {
background: var(--surface-1);
color: var(--text-muted);
}
.danger-btn {
background: transparent;
color: var(--text-muted);
}
.secondary-btn:hover,
.ghost-btn:hover,
.danger-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
</style>