From 2581861bc7e6e47979e50c458863532ab7797401 Mon Sep 17 00:00:00 2001 From: timlohse1104 Date: Sat, 28 Feb 2026 23:55:32 +0100 Subject: [PATCH 1/8] Added category history and category tab feature --- CHANGELOG.md | 3 + .../shared/common/types/src/lib/todo.dto.ts | 10 + .../todo/src/lib/schema/todo.schema.ts | 3 + frontend/src/lib/api/todo.api.ts | 1 + frontend/src/lib/components/todo/Todo.svelte | 30 ++- .../lib/components/todo/TodoCategories.svelte | 115 +++++++++++ .../src/lib/components/todo/TodoInput.svelte | 36 +++- .../src/lib/components/todo/TodoList.svelte | 194 +++++++++++++----- .../components/todo/TodoListOverlay.svelte | 2 + frontend/src/lib/config/de.json | 3 + frontend/src/lib/config/en.json | 3 + frontend/src/lib/types/todo.ts | 2 + frontend/src/lib/util/stores/store-todo.ts | 7 +- frontend/src/routes/todo/+page.svelte | 1 + 14 files changed, 355 insertions(+), 55 deletions(-) create mode 100644 frontend/src/lib/components/todo/TodoCategories.svelte diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d04bda..a56136d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [todo] Added server-side deletion for shared lists - when a shared list is deleted, it's removed from the backend. - [todo] Added cross-user deletion detection - automatically removes shared lists from all users when deleted by another user. - [todo] Added notification when a shared list is deleted by another user, with automatic list selection fallback. +- [todo] Added categories history tracking - all used categories are now stored in a persistent list similar to todo history. +- [todo] Added TodoCategories component displaying all categories with individual delete buttons and clear all functionality, positioned side-by-side with history on larger screens and stacked on mobile. +- [todo] Added Tab key autocomplete for category input fields using existing categories. - [memorandum] Added "Copy link URL" option to link context menu for easy URL copying to clipboard. - [memorandum] Added arrow-based reordering system with up/down buttons and "Move to Top/Bottom" context menu options for folders and links. - [memorandum] Added editable preset overlay with JSON validation, allowing direct editing of preset configuration with real-time validation and sync to localStorage/cloud. diff --git a/backend/libs/shared/common/types/src/lib/todo.dto.ts b/backend/libs/shared/common/types/src/lib/todo.dto.ts index ce4dfb1f..a816c568 100644 --- a/backend/libs/shared/common/types/src/lib/todo.dto.ts +++ b/backend/libs/shared/common/types/src/lib/todo.dto.ts @@ -53,6 +53,11 @@ export class SharedTodoListDto { @IsArray() history?: string[]; + @ApiProperty({ description: 'Categories used in todos', type: [String], required: false }) + @IsOptional() + @IsArray() + categories?: string[]; + @ApiProperty({ description: 'Version for optimistic locking' }) @IsNumber() version: number; @@ -116,6 +121,11 @@ export class UpdateSharedTodoListInputDto { @IsArray() history?: string[]; + @ApiProperty({ description: 'Categories used in todos', type: [String], required: false }) + @IsOptional() + @IsArray() + categories?: string[]; + @ApiProperty({ description: 'Current version for optimistic locking' }) @IsNumber() version: number; diff --git a/backend/libs/shared/provider/todo/src/lib/schema/todo.schema.ts b/backend/libs/shared/provider/todo/src/lib/schema/todo.schema.ts index 602f16ff..b49c5a22 100644 --- a/backend/libs/shared/provider/todo/src/lib/schema/todo.schema.ts +++ b/backend/libs/shared/provider/todo/src/lib/schema/todo.schema.ts @@ -36,6 +36,9 @@ export class SharedTodoList { @Prop({ type: [String], required: false, default: [] }) history?: string[]; + @Prop({ type: [String], required: false, default: [] }) + categories?: string[]; + @Prop({ type: Number, required: true, default: 1 }) version!: number; diff --git a/frontend/src/lib/api/todo.api.ts b/frontend/src/lib/api/todo.api.ts index f3e4088a..f24da395 100644 --- a/frontend/src/lib/api/todo.api.ts +++ b/frontend/src/lib/api/todo.api.ts @@ -38,6 +38,7 @@ export const updateSharedTodoList = async ( emoji: string; todos: any[]; history: string[]; + categories: string[]; version: number; }, ): Promise<{ status: number; data: SharedTodoListResponse | null }> => { diff --git a/frontend/src/lib/components/todo/Todo.svelte b/frontend/src/lib/components/todo/Todo.svelte index e61bba9e..9db196db 100644 --- a/frontend/src/lib/components/todo/Todo.svelte +++ b/frontend/src/lib/components/todo/Todo.svelte @@ -17,6 +17,7 @@ done, amount, category, + categories, deleteTodo, todoChecked, }: { @@ -25,6 +26,7 @@ done?: boolean; amount?: string; category?: string; + categories: string[]; deleteTodo: () => void; todoChecked: () => void; } = $props(); @@ -43,6 +45,27 @@ let isDone = $derived(done); // 8. FUNCTIONS + const autocompleteCategory = () => { + if (!newCategory) return; + + const matches = categories.filter((cat) => + cat.toLowerCase().startsWith(newCategory.toLowerCase()), + ); + + if (matches.length > 0) { + newCategory = matches[0]; + } + }; + + const handleCategoryKeydown = (e: KeyboardEvent) => { + if (e.key === 'Tab') { + e.preventDefault(); + autocompleteCategory(); + } else if (e.key === 'Enter') { + e.stopPropagation(); + saveEdit(); + } + }; const startEdit = () => { newTitle = todoTitle; @@ -127,12 +150,7 @@ bind:value={newCategory} placeholder={$t('page.todos.categoryPlaceholder')} class="category-input" - onkeydown={(e) => { - if (e.key === 'Enter') { - e.stopPropagation(); - saveEdit(); - } - }} + onkeydown={handleCategoryKeydown} /> + + {/each} + + - - - {/each} +
+
+ + + {#if list?.history?.length > 0} +
+
+ {#each list.history as entry (entry)} +
+ + +
+ {/each} +
+
-
- {:else} -
{$t('page.todos.list.historyEmpty')}
- {/if} - - + {:else} +
{$t('page.todos.list.historyEmpty')}
+ {/if} + + +
+
+ +
- + { + syncSharedList(); + }} + />
@@ -427,6 +502,7 @@ done={todo.done} amount={todo.amount} category={todo.category} + categories={list?.categories || []} deleteTodo={() => deleteTodo(todo.id)} todoChecked={() => checkTodo(todo.id)} /> @@ -451,6 +527,7 @@ done={todo.done} amount={todo.amount} category={todo.category} + categories={list?.categories || []} deleteTodo={() => deleteTodo(todo.id)} todoChecked={() => checkTodo(todo.id)} /> @@ -508,10 +585,35 @@ } } + .history_categories_container { + display: flex; + gap: 1rem; + width: 100%; + + @media #{$phone} { + flex-direction: column; + gap: 0; + } + } + .history_area { display: flex; flex-direction: column; - width: 100%; + flex: 1; + + @media #{$phone} { + width: 100%; + } + } + + .categories_area { + display: flex; + flex-direction: column; + flex: 1; + + @media #{$phone} { + width: 100%; + } } .history_list { diff --git a/frontend/src/lib/components/todo/TodoListOverlay.svelte b/frontend/src/lib/components/todo/TodoListOverlay.svelte index 57313154..33523de5 100644 --- a/frontend/src/lib/components/todo/TodoListOverlay.svelte +++ b/frontend/src/lib/components/todo/TodoListOverlay.svelte @@ -81,6 +81,7 @@ name: localListName, emoji: localListEmoji || '📝', history: [], + categories: [], todos: [], isShared: false, }; @@ -166,6 +167,7 @@ emoji: localListEmoji || '📝', todos: currentList.todos, history: currentList.history || [], + categories: currentList.categories || [], version: sharedList.version, }); } diff --git a/frontend/src/lib/config/de.json b/frontend/src/lib/config/de.json index 283bc427..0fa262b4 100644 --- a/frontend/src/lib/config/de.json +++ b/frontend/src/lib/config/de.json @@ -114,6 +114,7 @@ "page.todos.deleteTodo": "Eintrag löschen", "page.todos.editTodo": "Eintrag anpassen", "page.todos.deleteHistroy": "Verlauf löschen", + "page.todos.deleteCategories": "Kategorien löschen", "page.todos.sideMenu.title": "Deine Listen", "page.todos.sideMenu.description": "Einkaufen, Aufgaben und vieles mehr. Erstelle und bearbeite deine Listen hier. 📝", "page.todos.sideMenu.persistenceInfo": "Erstellte Listen werden im Browser auf deinem Gerät gespeichert. Bald wird auch hier eine Möglichkeit vorhanden sein, Listen online zu speichern und mit anderen zu teilen und live zu bearbeiten!", @@ -134,6 +135,8 @@ "page.todos.list.noListTitle": "Kein Listentitel vorhanden...", "page.todos.list.history": "Verlauf", "page.todos.list.historyEmpty": "Kein Verlauf vorhanden...", + "page.todos.list.categories": "Kategorien", + "page.todos.list.categoriesEmpty": "Keine Kategorien vorhanden...", "page.todos.list.emptyList": "Noch keine Einträge. Füge deinen ersten Eintrag hinzu!", "page.todos.list.emptyTitle": "Erstelle deine erste Liste.", "page.todos.list.emptySubtitle": "Klicke oben rechts auf den Menü-Button.", diff --git a/frontend/src/lib/config/en.json b/frontend/src/lib/config/en.json index f672bf03..c5f42414 100644 --- a/frontend/src/lib/config/en.json +++ b/frontend/src/lib/config/en.json @@ -114,6 +114,7 @@ "page.todos.deleteTodo": "Delete entry", "page.todos.editTodo": "Edit todo", "page.todos.deleteHistroy": "Delete history", + "page.todos.deleteCategories": "Delete categories", "page.todos.sideMenu.title": "Your lists", "page.todos.sideMenu.description": "Shopping, tasks and much more. Create your own lists and manage your todos. 📝", "page.todos.sideMenu.persistenceInfo": "Created lists are saved in the browser on your device. Soon there will also be an option to save lists online and share them with others and edit them live!", @@ -134,6 +135,8 @@ "page.todos.list.noListTitle": "No list title found...", "page.todos.list.history": "History", "page.todos.list.historyEmpty": "No history available...", + "page.todos.list.categories": "Categories", + "page.todos.list.categoriesEmpty": "No categories available...", "page.todos.list.emptyList": "No todos yet. Add your first entry!", "page.todos.list.emptyTitle": "Create your first list to start.", "page.todos.list.emptySubtitle": "Click the menu button in the top right corner.", diff --git a/frontend/src/lib/types/todo.ts b/frontend/src/lib/types/todo.ts index 4fcb55b1..c3a1bc5b 100644 --- a/frontend/src/lib/types/todo.ts +++ b/frontend/src/lib/types/todo.ts @@ -12,6 +12,7 @@ export type TodoList = { emoji: string; todos: Todo[]; history: string[]; + categories: string[]; isShared?: boolean; sharedId?: string; version?: number; @@ -23,6 +24,7 @@ export type SharedTodoListResponse = { emoji: string; todos: Todo[]; history: string[]; + categories: string[]; version: number; created: string; updated: string; diff --git a/frontend/src/lib/util/stores/store-todo.ts b/frontend/src/lib/util/stores/store-todo.ts index f7505fe3..8712867f 100644 --- a/frontend/src/lib/util/stores/store-todo.ts +++ b/frontend/src/lib/util/stores/store-todo.ts @@ -24,7 +24,12 @@ const getInitialValue = () => { const localStorageValue = JSON.parse(localStorage.getItem(localStorageKey)) || defaultValue; const backwardsCompatibleValue = ensureTodoListUUID(localStorageValue); - return backwardsCompatibleValue; + // Ensure categories property exists for backward compatibility + const withCategories = backwardsCompatibleValue.map((list: TodoList) => ({ + ...list, + categories: list.categories || [], + })); + return withCategories; } else { return defaultValue; } diff --git a/frontend/src/routes/todo/+page.svelte b/frontend/src/routes/todo/+page.svelte index 6848be29..85fe6bcb 100644 --- a/frontend/src/routes/todo/+page.svelte +++ b/frontend/src/routes/todo/+page.svelte @@ -163,6 +163,7 @@ emoji: sharedList.emoji, todos: sharedList.todos, history: sharedList.history || [], + categories: sharedList.categories || [], isShared: true, sharedId: sharedList._id, version: sharedList.version, From 04ebbfabec41ce652a628288ad6d68da07023d78 Mon Sep 17 00:00:00 2001 From: timlohse1104 Date: Sun, 1 Mar 2026 00:19:52 +0100 Subject: [PATCH 2/8] Added todo category tab hint --- CHANGELOG.md | 2 +- frontend/src/lib/components/todo/Todo.svelte | 66 ++++++++++++++++--- .../src/lib/components/todo/TodoInput.svelte | 63 ++++++++++++++++-- 3 files changed, 116 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a56136d9..ce36c91f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [todo] Added notification when a shared list is deleted by another user, with automatic list selection fallback. - [todo] Added categories history tracking - all used categories are now stored in a persistent list similar to todo history. - [todo] Added TodoCategories component displaying all categories with individual delete buttons and clear all functionality, positioned side-by-side with history on larger screens and stacked on mobile. -- [todo] Added Tab key autocomplete for category input fields using existing categories. +- [todo] Added Tab key autocomplete for category input fields using existing categories with visual hint showing matched category. - [memorandum] Added "Copy link URL" option to link context menu for easy URL copying to clipboard. - [memorandum] Added arrow-based reordering system with up/down buttons and "Move to Top/Bottom" context menu options for folders and links. - [memorandum] Added editable preset overlay with JSON validation, allowing direct editing of preset configuration with real-time validation and sync to localStorage/cloud. diff --git a/frontend/src/lib/components/todo/Todo.svelte b/frontend/src/lib/components/todo/Todo.svelte index 9db196db..0f982336 100644 --- a/frontend/src/lib/components/todo/Todo.svelte +++ b/frontend/src/lib/components/todo/Todo.svelte @@ -44,6 +44,16 @@ let todoCategory = $derived(category || ''); let isDone = $derived(done); + let categoryAutocomplete = $derived.by(() => { + if (!newCategory || newCategory.length === 0 || !isRenaming) return null; + + const matches = categories.filter((cat) => + cat.toLowerCase().startsWith(newCategory.toLowerCase()), + ); + + return matches.length > 0 ? matches[0] : null; + }); + // 8. FUNCTIONS const autocompleteCategory = () => { if (!newCategory) return; @@ -145,13 +155,20 @@ } }} /> - +
+ + {#if categoryAutocomplete && categoryAutocomplete !== newCategory} +
+ Tab → {categoryAutocomplete} +
+ {/if} +
- -
- {/each} - - + {/if} + + {#if items?.length > 0} +
+ {#each items as item (item)} +
+ + +
+ {/each} +
+ {:else} +
{emptyMessage}
+ {/if} + + +{/if} + + diff --git a/frontend/src/lib/components/todo/TodoList.svelte b/frontend/src/lib/components/todo/TodoList.svelte index 52c917e3..7a537f28 100644 --- a/frontend/src/lib/components/todo/TodoList.svelte +++ b/frontend/src/lib/components/todo/TodoList.svelte @@ -4,16 +4,12 @@ import type { Todo } from '$lib/types/todo'; import { todoStore } from '$lib/util/stores/store-todo'; import { initialized, t } from '$lib/util/translations'; - import Accordion from 'carbon-components-svelte/src/Accordion/Accordion.svelte'; - import AccordionItem from 'carbon-components-svelte/src/Accordion/AccordionItem.svelte'; - import Button from 'carbon-components-svelte/src/Button/Button.svelte'; import InlineNotification from 'carbon-components-svelte/src/Notification/InlineNotification.svelte'; import Toggle from 'carbon-components-svelte/src/Toggle/Toggle.svelte'; - import TrashCan from 'carbon-icons-svelte/lib/TrashCan.svelte'; import { onMount } from 'svelte'; - import TodoCategories from './TodoCategories.svelte'; import TodoComponent from './Todo.svelte'; import TodoInput from './TodoInput.svelte'; + import TodoItemList from './TodoItemList.svelte'; // 2. PROPS let { listId }: { listId: string } = $props(); @@ -420,56 +416,31 @@
- - - {#if list?.history?.length > 0} -
-
- {#each list.history as entry (entry)} -
- - -
- {/each} -
-
- {:else} -
{$t('page.todos.list.historyEmpty')}
- {/if} -
-
+
- { + window.dispatchEvent( + new CustomEvent('category-selected', { detail: { category } }), + ); + }} + onRemoveItem={removeCategory} + onClearAll={clearCategories} + preventFocusSteal={true} />
@@ -616,61 +587,10 @@ } } - .history_list { - display: flex; - align-items: start; - justify-content: space-between; - width: 100%; - } - - .history_entry_list { - display: flex; - flex-wrap: wrap; - } - hr { width: 100%; } - .history_tag_wrapper { - display: inline-flex; - align-items: center; - gap: 0.25rem; - margin: 0.25rem; - background: #393939; - border-radius: 4px; - padding: 0.25rem 0.5rem; - transition: background-color 0.2s ease; - - &:hover { - background: #525252; - } - } - - .history_tag { - background: transparent; - border: none; - color: white; - cursor: pointer; - padding: 0; - font-size: 0.875rem; - } - - .history_delete_btn { - background: transparent; - border: none; - color: rgba(255, 255, 255, 0.6); - cursor: pointer; - padding: 0; - font-size: 1rem; - line-height: 1; - transition: color 0.2s ease; - - &:hover { - color: #da1e28; - } - } - .view-toggle-container { margin: 1rem 0; display: flex; From 033a4fcc37d555f6e2b9d8322a12872cf0436888 Mon Sep 17 00:00:00 2001 From: timlohse1104 Date: Sun, 1 Mar 2026 01:49:10 +0100 Subject: [PATCH 5/8] Refactored new todo entry parts into own component --- CHANGELOG.md | 1 + .../components/todo/TodoInputSection.svelte | 64 +++++++++++++++++++ .../lib/components/todo/TodoItemList.svelte | 2 +- .../src/lib/components/todo/TodoList.svelte | 20 +----- 4 files changed, 69 insertions(+), 18 deletions(-) create mode 100644 frontend/src/lib/components/todo/TodoInputSection.svelte diff --git a/CHANGELOG.md b/CHANGELOG.md index db169c80..9fab9c45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - [todo] Refactored history and categories into a reusable TodoItemList component, reducing code duplication and improving maintainability. +- [todo] Created TodoInputSection component wrapping view toggle and new todo inputs in an accordion for consistent UI structure. - [todo] Redesigned clear history and clear categories buttons - moved to accordion title next to component name as simple "×" with red hover effect for cleaner UI. - [backend] Updated all backend dependencies to latest compatible versions: NX 20.8.2 → 22.5.2, TypeScript 5.8.3 → 5.9.3, @swc/core 1.11.x → 1.15.13, NestJS packages to 11.1.14, Fastify 5.0.0 → 5.7.4, pino-http 10.2.0 → 11.0.0, cron 3.x → 4.4.0, and 50+ other packages. - [backend] Upgraded Mongoose from 8.1.1 to 9.2.2 with complete API migration (FilterQuery → mongodb.Filter) across 10 files. diff --git a/frontend/src/lib/components/todo/TodoInputSection.svelte b/frontend/src/lib/components/todo/TodoInputSection.svelte new file mode 100644 index 00000000..7e833b6f --- /dev/null +++ b/frontend/src/lib/components/todo/TodoInputSection.svelte @@ -0,0 +1,64 @@ + + +{#if $initialized} + + +
+ {$t('page.todos.newTodo')} +
+
+
+ +
+ +
+
+
+{/if} + + diff --git a/frontend/src/lib/components/todo/TodoItemList.svelte b/frontend/src/lib/components/todo/TodoItemList.svelte index b46f760a..36027c07 100644 --- a/frontend/src/lib/components/todo/TodoItemList.svelte +++ b/frontend/src/lib/components/todo/TodoItemList.svelte @@ -1,6 +1,6 @@ {#if $initialized} - - -
- {$t('page.todos.newTodo')} -
-
-
- -
- -
-
-
+
+ + +
{/if} diff --git a/frontend/src/lib/components/todo/TodoItemList.svelte b/frontend/src/lib/components/todo/TodoItemList.svelte index 36027c07..25c4bfec 100644 --- a/frontend/src/lib/components/todo/TodoItemList.svelte +++ b/frontend/src/lib/components/todo/TodoItemList.svelte @@ -1,11 +1,26 @@ {#if $initialized} - - -
- {title} - {#if items?.length > 0} - - {/if} -
+ + + {/if}