Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- [todo] Migrated todo route and all corresponding components (TodoList, TodoInput, Todo, TodoListOverlay, +page) to Svelte 5 runes syntax ($props, $state, $derived, $effect) for improved reactivity and type safety.
- [todo] Enhanced Todo component with improved UI/UX including clickable rows, better visual feedback, and proper accessibility.
- [todo] Improved TodoList with sorted todos (unchecked first), better state management using $derived, and enhanced history management.
- [todo] Enhanced TodoListOverlay with better form handling, proper state management, and automatic list selection after creation.
- [todo] Updated translations for global menu button text to be more descriptive.
- [food-scan] Migrated food-scan route and all corresponding components (FoodScan, ContentOutput, DebugInformation) to Svelte 5 runes syntax ($props, $state, $derived, $effect) for improved reactivity and type safety.
- [uno-sort] Migrated uno-sort route and ToggledApplicationInfo component to Svelte 5 runes syntax ($props, $state, $derived, $effect) for improved reactivity and type safety.
- [settings] Migrated settings route and all corresponding components (+page, SettingsDashboard, OnlinePersistenceCheck, ThemeSwitch, LanguageSwitch, BackgroundSwitch, IdentifierInformation) to Svelte 5 runes syntax ($props, $state, $derived, $effect) for improved reactivity and type safety.
Expand Down
100 changes: 80 additions & 20 deletions frontend/src/lib/components/todo/Todo.svelte
Original file line number Diff line number Diff line change
@@ -1,33 +1,55 @@
<script lang="ts">
// 1. IMPORTS
import type { Todo } from '$lib/types/todo.ts';
import { t } from '$lib/util/translations';
import Button from 'carbon-components-svelte/src/Button/Button.svelte';
import Checkbox from 'carbon-components-svelte/src/Checkbox/Checkbox.svelte';
import TrashCan from 'carbon-icons-svelte/lib/TrashCan.svelte';

export let todo: Todo;
export let deleteTodo;
export let todoChecked;
// 2. PROPS
let {
todo,
deleteTodo,
todoChecked,
}: {
todo: Todo;
deleteTodo: () => void;
todoChecked: () => void;
} = $props();

// 5. DERIVED
let todoTitle = $derived(todo.title);
let isDone = $derived(todo.done);
</script>

<section>
<Checkbox
bind:checked={todo['done']}
labelText="Label text"
on:click={todoChecked}
>
<span slot="labelText" class={todo?.done ? 'ml1 striked' : 'ml1'}>
{todo?.title}
<section onclick={todoChecked}>
<label class="checkbox-wrapper">
<input
type="checkbox"
checked={isDone}
onchange={todoChecked}
class="hidden-checkbox"
/>
<Checkbox checked={isDone} readonly />
<span class={isDone ? 'striked todo-label' : 'todo-label'}>
{todoTitle}
</span>
</Checkbox>
<Button
kind="danger"
size="small"
iconDescription={$t('page.todos.deleteTodo')}
tooltipAlignment="end"
icon={TrashCan}
on:click={deleteTodo}
/>
</label>
<div
onclick={(e) => e.stopPropagation()}
role="button"
tabindex="-1"
style="display: contents;"
>
<Button
kind="danger"
size="small"
iconDescription={$t('page.todos.deleteTodo')}
tooltipAlignment="end"
icon={TrashCan}
onclick={deleteTodo}
/>
</div>
</section>

<style lang="scss">
Expand All @@ -36,9 +58,47 @@
justify-content: space-between;
align-items: center;
margin-top: 0.5rem;
gap: 0.5rem;
padding: 0.5rem;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
background-color: transparent;

&:hover {
background-color: rgba(255, 255, 255, 0.08);
transform: translateX(4px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
}

.checkbox-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
}

.checkbox-wrapper :global(.bx--checkbox-wrapper) {
margin: 0;
flex-shrink: 0;
pointer-events: none;
}

.hidden-checkbox {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}

.todo-label {
user-select: none;
font-weight: 600;
}

.striked {
text-decoration: line-through;
opacity: 0.6;
font-weight: 100;
}
</style>
38 changes: 22 additions & 16 deletions frontend/src/lib/components/todo/TodoInput.svelte
Original file line number Diff line number Diff line change
@@ -1,35 +1,41 @@
<script lang="ts">
// 1. IMPORTS
import { todoStore } from '$lib/util/stores/store-todo';
import { initialized, t } from '$lib/util/translations';
import Add from 'carbon-icons-svelte/lib/Add.svelte';
import InputWithButton from '../shared/custom-carbon-components/InputWithButton.svelte';

export let listId;
// 2. PROPS
let { listId }: { listId: string } = $props();

let newTodoName = '';
// 4. STATE
let newTodoName = $state('');

// 8. FUNCTIONS
const saveTodo = () => {
if (newTodoName) {
todoStore.update((todoListArray) => {
const list = todoListArray.find((list) => list.id === listId);
list.todos.push({
id: crypto.randomUUID(),
title: newTodoName,
done: false,
return todoListArray.map((list) => {
if (list.id === listId) {
return {
...list,
todos: [
...list.todos,
{
id: crypto.randomUUID(),
title: newTodoName,
done: false,
},
],
history: Array.from(new Set([...list.history, newTodoName])),
};
}
return list;
});
return [...todoListArray];
});
addToHistory(newTodoName);
newTodoName = '';
}
};
const addToHistory = (todoName) => {
todoStore.update((todoListArray) => {
const list = todoListArray.find((list) => list.id === listId);
list.history = Array.from(new Set(list.history).add(todoName));
return todoListArray;
});
};
</script>

{#if $initialized}
Expand Down
112 changes: 81 additions & 31 deletions frontend/src/lib/components/todo/TodoList.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
// 1. IMPORTS
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';
Expand All @@ -9,33 +10,70 @@
import Todo from './Todo.svelte';
import TodoInput from './TodoInput.svelte';

export let listId;
// 2. PROPS
let { listId }: { listId: string } = $props();

$: list = $todoStore.find((list) => list.id === listId);
$: if (listId) console.log('listId', listId);
$: if (list) console.log('list', list);
// 5. DERIVED
let list = $derived.by(() => {
const allLists = $todoStore;
const foundList = listId ? allLists.find((l) => l.id === listId) : undefined;

if (foundList) {
// Sort todos: unchecked first, checked last
const sortedTodos = [...foundList.todos].sort((a, b) => {
if (a.done === b.done) return 0;
return a.done ? 1 : -1;
});

return {
...foundList,
todos: sortedTodos,
};
}

return undefined;
});

// 8. FUNCTIONS
const deleteTodo = (todoId: string) => {
todoStore.update((todoListArray) => {
const list = todoListArray.find((list) => list.id === listId);
const todoIndex = list.todos.findIndex((todo) => todo.id === todoId);
list.todos.splice(todoIndex, 1);
return todoListArray;
return todoListArray.map((list) => {
if (list.id === listId) {
return {
...list,
todos: list.todos.filter((todo) => todo.id !== todoId),
};
}
return list;
});
});
};
const checkTodo = (todoId: string) => {
todoStore.update((todoListArray) => {
const list = todoListArray.find((list) => list.id === listId);
const todoIndex = list.todos.findIndex((todo) => todo.id === todoId);
list.todos[todoIndex].done = !list.todos[todoIndex].done;
return todoListArray;
return todoListArray.map((list) => {
if (list.id === listId) {
return {
...list,
todos: list.todos.map((todo) =>
todo.id === todoId ? { ...todo, done: !todo.done } : todo,
),
};
}
return list;
});
});
};
const clearHistory = () => {
todoStore.update((todoListArray) => {
const list = todoListArray.find((list) => list.id === listId);
list.history = [];
return todoListArray;
return todoListArray.map((list) => {
if (list.id === listId) {
return {
...list,
history: [],
};
}
return list;
});
});
};
const selectRandomTagColor = ():
Expand Down Expand Up @@ -80,6 +118,7 @@
];
return colors[Math.floor(Math.random() * colors.length)];
};

const removeEntryFromHistory = (event) => {
const tagText =
event.explicitOriginalTarget.parentElement.parentElement.textContent.trim() ||
Expand All @@ -88,11 +127,15 @@
if (!tagText) return;

todoStore.update((todoListArray) => {
const list = todoListArray.find((list) => list.id === listId);
list.history = list.history.filter((entry) => {
return entry !== tagText;
return todoListArray.map((list) => {
if (list.id === listId) {
return {
...list,
history: list.history.filter((entry) => entry !== tagText),
};
}
return list;
});
return todoListArray;
});
};
const readdTodoFromHistory = (event) => {
Expand All @@ -101,9 +144,18 @@
if (!tagText) return;

todoStore.update((todoListArray) => {
const list = todoListArray.find((list) => list.id === listId);
list.todos.push({ id: crypto.randomUUID(), title: tagText, done: false });
return [...todoListArray];
return todoListArray.map((list) => {
if (list.id === listId) {
return {
...list,
todos: [
...list.todos,
{ id: crypto.randomUUID(), title: tagText, done: false },
],
};
}
return list;
});
});
};
</script>
Expand All @@ -126,7 +178,7 @@
{#if list?.history?.length > 0}
<div class="history_list">
<div class="history_entry_list">
{#each list?.history as entry}
{#each list.history as entry (entry)}
<Tag
filter
interactive
Expand Down Expand Up @@ -162,14 +214,12 @@
</div>

<div class="mt1 list_content">
{#each list?.todos as todo}
{#if todo}
<Todo
{todo}
deleteTodo={() => deleteTodo(todo.id)}
todoChecked={() => checkTodo(todo.id)}
/>
{/if}
{#each list?.todos || [] as todo (todo.id)}
<Todo
{todo}
deleteTodo={() => deleteTodo(todo.id)}
todoChecked={() => checkTodo(todo.id)}
/>
{/each}
</div>
</div>
Expand Down
Loading
Loading