feat(tasks): persist view mode and support ?view=kanban URL parameter

View mode (list/kanban) is now saved to localStorage and restored on
page load. URL parameter ?view=kanban takes precedence, enabling tablet
kiosk setups to default to Kanban view. Toggle buttons reflect the
active view correctly on initial render.

Closes #17
This commit is contained in:
Ulas
2026-04-04 22:34:29 +02:00
parent 2c36fa0307
commit 0421b540cd
4 changed files with 26 additions and 9 deletions
+7
View File
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.9.1] - 2026-04-04
### Added
- Persist task view mode (list/kanban) across sessions via localStorage (#17)
- Support URL parameter `?view=kanban` to open tasks directly in Kanban view - ideal for tablet kiosk setups
- View toggle button reflects the persisted/URL-driven view on page load
## [0.9.0] - 2026-04-04
### Added
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "oikos",
"version": "0.9.0",
"version": "0.9.1",
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
"main": "server/index.js",
"type": "module",
+15 -5
View File
@@ -356,7 +356,7 @@ let state = {
users: [],
filters: { status: '', priority: '', assigned_to: '' },
groupMode: 'category', // 'category' | 'due'
viewMode: 'list', // 'list' | 'kanban'
viewMode: 'list', // 'list' | 'kanban' (resolved at render time)
expandedTasks: new Set(),
dragTaskId: null,
};
@@ -872,6 +872,7 @@ function wireViewToggle(container) {
toggle.querySelectorAll('[data-view]').forEach((btn) => {
btn.addEventListener('click', () => {
state.viewMode = btn.dataset.view;
localStorage.setItem('oikos-tasks-view', state.viewMode);
toggle.querySelectorAll('[data-view]').forEach((b) =>
b.classList.toggle('group-toggle__btn--active', b.dataset.view === state.viewMode)
);
@@ -962,23 +963,32 @@ function wireTaskList(container) {
// --------------------------------------------------------
export async function render(container, { user }) {
// Initiales Skeleton
// View-Mode: URL-Parameter > localStorage > Default 'list'
const urlView = new URLSearchParams(window.location.search).get('view');
const savedView = localStorage.getItem('oikos-tasks-view');
state.viewMode = (urlView === 'kanban' || urlView === 'list') ? urlView
: (savedView === 'kanban' || savedView === 'list') ? savedView
: 'list';
const isKanban = state.viewMode === 'kanban';
// Initiales Skeleton (all values are from i18n keys or hardcoded constants, no user data)
container.innerHTML = `
<div class="tasks-page">
<div class="tasks-toolbar">
<h1 class="tasks-toolbar__title">${t('tasks.title')}</h1>
<div class="tasks-toolbar__actions">
<div class="group-toggle" id="view-toggle">
<button class="group-toggle__btn group-toggle__btn--active" data-view="list"
<button class="group-toggle__btn ${isKanban ? '' : 'group-toggle__btn--active'}" data-view="list"
title="${t('tasks.listView')}" aria-label="${t('tasks.listView')}">
<i data-lucide="list" style="width:14px;height:14px;pointer-events:none" aria-hidden="true"></i>
</button>
<button class="group-toggle__btn" data-view="kanban"
<button class="group-toggle__btn ${isKanban ? 'group-toggle__btn--active' : ''}" data-view="kanban"
title="${t('tasks.kanbanView')}" aria-label="${t('tasks.kanbanView')}">
<i data-lucide="columns" style="width:14px;height:14px;pointer-events:none" aria-hidden="true"></i>
</button>
</div>
<div class="group-toggle" id="group-mode-toggle">
<div class="group-toggle" id="group-mode-toggle" ${isKanban ? 'style="display:none"' : ''}>
<button class="group-toggle__btn group-toggle__btn--active" data-mode="category">${t('tasks.categoryLabel')}</button>
<button class="group-toggle__btn" data-mode="due">${t('tasks.dueDateLabel')}</button>
</div>
+3 -3
View File
@@ -12,9 +12,9 @@
* API: Immer Netzwerk (kein Caching von Nutzerdaten)
*/
const SHELL_CACHE = 'oikos-shell-v24';
const PAGES_CACHE = 'oikos-pages-v24';
const ASSETS_CACHE = 'oikos-assets-v24';
const SHELL_CACHE = 'oikos-shell-v25';
const PAGES_CACHE = 'oikos-pages-v25';
const ASSETS_CACHE = 'oikos-assets-v25';
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE];
// App-Shell: sofort benötigt für ersten Render