feat: Implement tasks feature using NGRX signals and remove the old counter store, alongside general project configuration and skill documentation updates.
continuous-integration/drone/pr Build is passing
continuous-integration/drone/pr Build is passing
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
import { computed, inject } from '@angular/core';
|
||||
import {
|
||||
addEntity,
|
||||
removeEntities,
|
||||
setAllEntities,
|
||||
updateEntity,
|
||||
withEntities,
|
||||
} from '@ngrx/signals/entities';
|
||||
import {
|
||||
signalStore,
|
||||
withComputed,
|
||||
withHooks,
|
||||
withMethods,
|
||||
withState,
|
||||
} from '@ngrx/signals';
|
||||
import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import {
|
||||
setError,
|
||||
setLoaded,
|
||||
setLoading,
|
||||
updateState,
|
||||
withCallState,
|
||||
withDevtools,
|
||||
withLocalStorage,
|
||||
withStorageSync,
|
||||
} from '@angular-architects/ngrx-toolkit';
|
||||
import { EMPTY, pipe } from 'rxjs';
|
||||
import { catchError, switchMap, tap } from 'rxjs/operators';
|
||||
import { Task, TaskFilter, TaskPriority } from './task.model';
|
||||
import { TasksApiService } from './tasks-api.service';
|
||||
|
||||
interface TasksState {
|
||||
filter: TaskFilter;
|
||||
draftPriority: TaskPriority;
|
||||
searchTerm: string;
|
||||
lastSyncedAt: string | null;
|
||||
}
|
||||
|
||||
const initialState: TasksState = {
|
||||
filter: 'all',
|
||||
draftPriority: 'medium',
|
||||
searchTerm: '',
|
||||
lastSyncedAt: null,
|
||||
};
|
||||
|
||||
function matchesFilter(task: Task, filter: TaskFilter): boolean {
|
||||
switch (filter) {
|
||||
case 'active':
|
||||
return !task.completed;
|
||||
case 'completed':
|
||||
return task.completed;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const TasksStore = signalStore(
|
||||
{ providedIn: 'root' },
|
||||
withState(initialState),
|
||||
withEntities<Task>(),
|
||||
withCallState(),
|
||||
withDevtools('tasks-store'),
|
||||
withStorageSync(
|
||||
{
|
||||
key: 'ngrx-playground.tasks',
|
||||
select: (state) => ({
|
||||
ids: state.ids,
|
||||
entityMap: state.entityMap,
|
||||
filter: state.filter,
|
||||
draftPriority: state.draftPriority,
|
||||
searchTerm: state.searchTerm,
|
||||
lastSyncedAt: state.lastSyncedAt,
|
||||
}),
|
||||
},
|
||||
withLocalStorage(),
|
||||
),
|
||||
withComputed((store) => ({
|
||||
filteredTasks: computed(() => {
|
||||
const normalizedSearch = store.searchTerm().trim().toLowerCase();
|
||||
|
||||
return store.entities().filter((task) => {
|
||||
const matchesText = normalizedSearch.length === 0 || task.title.toLowerCase().includes(normalizedSearch);
|
||||
|
||||
return matchesFilter(task, store.filter()) && matchesText;
|
||||
});
|
||||
}),
|
||||
totalCount: computed(() => store.entities().length),
|
||||
activeCount: computed(() => store.entities().filter((task) => !task.completed).length),
|
||||
completedCount: computed(() => store.entities().filter((task) => task.completed).length),
|
||||
hasCompletedTasks: computed(() => store.entities().some((task) => task.completed)),
|
||||
emptyStateMessage: computed(() => {
|
||||
if (store.loading()) {
|
||||
return 'Loading tasks...';
|
||||
}
|
||||
|
||||
if (store.error()) {
|
||||
return 'The task list could not be refreshed.';
|
||||
}
|
||||
|
||||
if (store.entities().length === 0) {
|
||||
return 'No tasks saved yet. Create the first one.';
|
||||
}
|
||||
|
||||
return 'No tasks match the current filters.';
|
||||
}),
|
||||
lastSyncedLabel: computed(() => {
|
||||
const value = store.lastSyncedAt();
|
||||
return value ? new Date(value).toLocaleTimeString('en-US') : 'Never';
|
||||
}),
|
||||
})),
|
||||
methodsStore(),
|
||||
withHooks({
|
||||
onInit(store) {
|
||||
if (store.entities().length === 0) {
|
||||
store.loadTasks();
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
function methodsStore() {
|
||||
return withMethods((store, api = inject(TasksApiService)) => ({
|
||||
loadTasks: rxMethod<void>(
|
||||
pipe(
|
||||
tap(() => updateState(store, 'tasks load started', setLoading())),
|
||||
switchMap(() =>
|
||||
api.getTasks().pipe(
|
||||
tap((tasks) =>
|
||||
updateState(
|
||||
store,
|
||||
'tasks load succeeded',
|
||||
setAllEntities(tasks),
|
||||
{ lastSyncedAt: new Date().toISOString() },
|
||||
setLoaded(),
|
||||
),
|
||||
),
|
||||
catchError((error: unknown) => {
|
||||
updateState(store, 'tasks load failed', setError(error));
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
createTask: rxMethod<{ title: string; priority: TaskPriority }>(
|
||||
pipe(
|
||||
tap(() => updateState(store, 'task create started', setLoading())),
|
||||
switchMap(({ title, priority }) =>
|
||||
api.createTask(title, priority).pipe(
|
||||
tap((task) =>
|
||||
updateState(store, 'task create succeeded', addEntity(task), { lastSyncedAt: task.updatedAt }, setLoaded()),
|
||||
),
|
||||
catchError((error: unknown) => {
|
||||
updateState(store, 'task create failed', setError(error));
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
toggleTask: rxMethod<string>(
|
||||
pipe(
|
||||
tap(() => updateState(store, 'task toggle started', setLoading())),
|
||||
switchMap((taskId) =>
|
||||
api.toggleTask(taskId).pipe(
|
||||
tap((task) =>
|
||||
updateState(
|
||||
store,
|
||||
'task toggle succeeded',
|
||||
updateEntity({ id: task.id, changes: task }),
|
||||
{ lastSyncedAt: task.updatedAt },
|
||||
setLoaded(),
|
||||
),
|
||||
),
|
||||
catchError((error: unknown) => {
|
||||
updateState(store, 'task toggle failed', setError(error));
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
archiveCompleted: rxMethod<void>(
|
||||
pipe(
|
||||
tap(() => updateState(store, 'completed tasks archive started', setLoading())),
|
||||
switchMap(() =>
|
||||
api.archiveCompleted().pipe(
|
||||
tap((archivedIds) =>
|
||||
updateState(
|
||||
store,
|
||||
'completed tasks archive succeeded',
|
||||
removeEntities(archivedIds),
|
||||
{ lastSyncedAt: new Date().toISOString() },
|
||||
setLoaded(),
|
||||
),
|
||||
),
|
||||
catchError((error: unknown) => {
|
||||
updateState(store, 'completed tasks archive failed', setError(error));
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
setFilter(filter: TaskFilter): void {
|
||||
updateState(store, 'task filter changed', { filter });
|
||||
},
|
||||
|
||||
setDraftPriority(priority: TaskPriority): void {
|
||||
updateState(store, 'task priority draft changed', { draftPriority: priority });
|
||||
},
|
||||
|
||||
setSearchTerm(searchTerm: string): void {
|
||||
updateState(store, 'task search changed', { searchTerm });
|
||||
},
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user