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:
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.9.0] - 2026-04-04
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oikos",
|
"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.",
|
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
|
||||||
"main": "server/index.js",
|
"main": "server/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+15
-5
@@ -356,7 +356,7 @@ let state = {
|
|||||||
users: [],
|
users: [],
|
||||||
filters: { status: '', priority: '', assigned_to: '' },
|
filters: { status: '', priority: '', assigned_to: '' },
|
||||||
groupMode: 'category', // 'category' | 'due'
|
groupMode: 'category', // 'category' | 'due'
|
||||||
viewMode: 'list', // 'list' | 'kanban'
|
viewMode: 'list', // 'list' | 'kanban' (resolved at render time)
|
||||||
expandedTasks: new Set(),
|
expandedTasks: new Set(),
|
||||||
dragTaskId: null,
|
dragTaskId: null,
|
||||||
};
|
};
|
||||||
@@ -872,6 +872,7 @@ function wireViewToggle(container) {
|
|||||||
toggle.querySelectorAll('[data-view]').forEach((btn) => {
|
toggle.querySelectorAll('[data-view]').forEach((btn) => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
state.viewMode = btn.dataset.view;
|
state.viewMode = btn.dataset.view;
|
||||||
|
localStorage.setItem('oikos-tasks-view', state.viewMode);
|
||||||
toggle.querySelectorAll('[data-view]').forEach((b) =>
|
toggle.querySelectorAll('[data-view]').forEach((b) =>
|
||||||
b.classList.toggle('group-toggle__btn--active', b.dataset.view === state.viewMode)
|
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 }) {
|
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 = `
|
container.innerHTML = `
|
||||||
<div class="tasks-page">
|
<div class="tasks-page">
|
||||||
<div class="tasks-toolbar">
|
<div class="tasks-toolbar">
|
||||||
<h1 class="tasks-toolbar__title">${t('tasks.title')}</h1>
|
<h1 class="tasks-toolbar__title">${t('tasks.title')}</h1>
|
||||||
<div class="tasks-toolbar__actions">
|
<div class="tasks-toolbar__actions">
|
||||||
<div class="group-toggle" id="view-toggle">
|
<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')}">
|
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>
|
<i data-lucide="list" style="width:14px;height:14px;pointer-events:none" aria-hidden="true"></i>
|
||||||
</button>
|
</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')}">
|
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>
|
<i data-lucide="columns" style="width:14px;height:14px;pointer-events:none" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 group-toggle__btn--active" data-mode="category">${t('tasks.categoryLabel')}</button>
|
||||||
<button class="group-toggle__btn" data-mode="due">${t('tasks.dueDateLabel')}</button>
|
<button class="group-toggle__btn" data-mode="due">${t('tasks.dueDateLabel')}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3
-3
@@ -12,9 +12,9 @@
|
|||||||
* API: Immer Netzwerk (kein Caching von Nutzerdaten)
|
* API: Immer Netzwerk (kein Caching von Nutzerdaten)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SHELL_CACHE = 'oikos-shell-v24';
|
const SHELL_CACHE = 'oikos-shell-v25';
|
||||||
const PAGES_CACHE = 'oikos-pages-v24';
|
const PAGES_CACHE = 'oikos-pages-v25';
|
||||||
const ASSETS_CACHE = 'oikos-assets-v24';
|
const ASSETS_CACHE = 'oikos-assets-v25';
|
||||||
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE];
|
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE];
|
||||||
|
|
||||||
// App-Shell: sofort benötigt für ersten Render
|
// App-Shell: sofort benötigt für ersten Render
|
||||||
|
|||||||
Reference in New Issue
Block a user