From 992c9095161d96a746ee6bfd53e77b57ad4c802e Mon Sep 17 00:00:00 2001 From: iptoux Date: Fri, 18 Apr 2025 11:33:05 +0200 Subject: [PATCH 01/24] Add InitialChoiceComponent with basic structure and tests Introduces the InitialChoiceComponent, including its HTML, CSS, and TypeScript files, along with a basic test suite. This sets up the initial structure for further development of the component. --- .../initial-choice.component.css | 0 .../initial-choice.component.html | 1 + .../initial-choice.component.spec.ts | 23 +++++++++++++++++++ .../initial-choice.component.ts | 11 +++++++++ 4 files changed, 35 insertions(+) create mode 100644 src/app/components/initial-choice/initial-choice.component.css create mode 100644 src/app/components/initial-choice/initial-choice.component.html create mode 100644 src/app/components/initial-choice/initial-choice.component.spec.ts create mode 100644 src/app/components/initial-choice/initial-choice.component.ts diff --git a/src/app/components/initial-choice/initial-choice.component.css b/src/app/components/initial-choice/initial-choice.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/initial-choice/initial-choice.component.html b/src/app/components/initial-choice/initial-choice.component.html new file mode 100644 index 0000000..f79450f --- /dev/null +++ b/src/app/components/initial-choice/initial-choice.component.html @@ -0,0 +1 @@ +

initial-choice works!

diff --git a/src/app/components/initial-choice/initial-choice.component.spec.ts b/src/app/components/initial-choice/initial-choice.component.spec.ts new file mode 100644 index 0000000..ebb50d6 --- /dev/null +++ b/src/app/components/initial-choice/initial-choice.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InitialChoiceComponent } from './initial-choice.component'; + +describe('InitialChoiceComponent', () => { + let component: InitialChoiceComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InitialChoiceComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(InitialChoiceComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/initial-choice/initial-choice.component.ts b/src/app/components/initial-choice/initial-choice.component.ts new file mode 100644 index 0000000..28bedc4 --- /dev/null +++ b/src/app/components/initial-choice/initial-choice.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-initial-choice', + imports: [], + templateUrl: './initial-choice.component.html', + styleUrl: './initial-choice.component.css' +}) +export class InitialChoiceComponent { + +} From c91f1477c9f8a4ff3eb665f0c80c9f57f5750b24 Mon Sep 17 00:00:00 2001 From: iptoux Date: Sat, 19 Apr 2025 22:09:49 +0200 Subject: [PATCH 02/24] Add initial choice selection functionality Implemented an interface and UI for selecting Offline or Online modes during the initial setup. Integrated dark mode compatibility, countdown timer, and navigation logic based on user choice, updating settings accordingly. --- .../initial-choice.component.css | 33 +++++++++ .../initial-choice.component.html | 50 ++++++++++++- .../initial-choice.component.ts | 71 ++++++++++++++++++- src/app/interfaces/settings.ts | 1 + src/assets/images/offline-svgrepo-com.svg | 2 + src/assets/images/online-svgrepo-com.svg | 2 + 6 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 src/assets/images/offline-svgrepo-com.svg create mode 100644 src/assets/images/online-svgrepo-com.svg diff --git a/src/app/components/initial-choice/initial-choice.component.css b/src/app/components/initial-choice/initial-choice.component.css index e69de29..f694abf 100644 --- a/src/app/components/initial-choice/initial-choice.component.css +++ b/src/app/components/initial-choice/initial-choice.component.css @@ -0,0 +1,33 @@ +.dark-theme { + img { + filter: invert(1); + } + .card-choice { + cursor: pointer; + color: white; + opacity: 0.5; + transition: all 0.2s ease-in-out; + } + .card-choice:hover { + opacity: 1; + transition: all 0.2s ease-in-out; + } +} + +.card-choice { + cursor: pointer; + opacity: 0.5; + transition: all 0.2s ease-in-out; +} +.card-choice:hover { + opacity: 1; + transition: all 0.2s ease-in-out; +} + +.countdown-timer { + font-size: 1.2rem; + font-weight: bold; + text-align: center; + margin-bottom: 1rem; + color: #dc3545; /* Bootstrap danger color */ +} diff --git a/src/app/components/initial-choice/initial-choice.component.html b/src/app/components/initial-choice/initial-choice.component.html index f79450f..b7c4ac5 100644 --- a/src/app/components/initial-choice/initial-choice.component.html +++ b/src/app/components/initial-choice/initial-choice.component.html @@ -1 +1,49 @@ -

initial-choice works!

+
+
+ @if(showCountdown) { +
+ Redirecting in {{countdown}} seconds... +
+ + } +

Initial Choice

+

+ Welcome aboard! As this is your first time using our application, please decide whether you’d prefer Offline Mode—offering full + functionality without an internet connection and storing all your data securely on your device—or Online Mode, which provides seamless + access anytime you’re connected, with your information synchronized safely to the cloud. Choose the option that best fits your + needs, and let’s get started! +

+
+ +
+
+
+
+
+ offline +
+
+
Offline
+

Full functionality without an internet connection and storing all your data securely on your device.

+
+
+
+
+ +
+
+
+
+ offline +
+
+
Online
+

provides seamless access anytime you’re connected, with your information synchronized safely to the cloud.

+
+
+
+
+
+
diff --git a/src/app/components/initial-choice/initial-choice.component.ts b/src/app/components/initial-choice/initial-choice.component.ts index 28bedc4..a4403e7 100644 --- a/src/app/components/initial-choice/initial-choice.component.ts +++ b/src/app/components/initial-choice/initial-choice.component.ts @@ -1,11 +1,78 @@ -import { Component } from '@angular/core'; +import {Component, inject} from '@angular/core'; +import {SettingsService} from '../../services/settings.service'; +import {NgClass, NgOptimizedImage} from '@angular/common'; +import {DarkModeService} from '../../services/dark-mode.service'; +import {NotificationService} from '../../services/notification.service'; +import {Router} from '@angular/router'; @Component({ selector: 'app-initial-choice', - imports: [], + imports: [ + NgClass, + NgOptimizedImage + ], templateUrl: './initial-choice.component.html', styleUrl: './initial-choice.component.css' }) export class InitialChoiceComponent { + private readonly initialChoice: string|undefined = undefined; + private settingsService: SettingsService = inject(SettingsService); + private notificationService: NotificationService = inject(NotificationService); + + public countdown: number = 5; + public showCountdown: boolean = false; + + constructor(private darkModeService: DarkModeService,private router: Router + ) { + this.initialChoice = this.settingsService.getSettings()()[0]?.initialChoice; + } + + get isDarkMode(): boolean { + return this.darkModeService.isDarkMode(); + } + + isInitialChoice(): boolean { + return this.initialChoice !== undefined; + } + + setInitialChoice(choice: string) { + const currentSettings = this.settingsService.getSettings()(); + + // Create a new settings array with the initialChoice updated + const updatedSettings = currentSettings.map((setting, index) => { + if (index === 0) { + // Update the first settings object with the new initialChoice + return { + ...setting, + initialChoice: choice + }; + } + return setting; + }); + + // Update the settings with the new array + this.settingsService.updateSettings(updatedSettings); + console.log("Initial choice set to: " + choice); + if (choice === 'offline') { + // Redirect to home + void this.router.navigate(['/account/create/local']); + } else if(choice === 'online') { + console.log("Online mode not implemented yet.") + this.notificationService.addNotification("Info:", "Online mode not implemented yet."); + + // sleep and show countdown 5 sec + this.showCountdown = true; + const intervalId = setInterval(() => { + this.countdown--; + + if (this.countdown <= 0) { + clearInterval(intervalId); + window.location.href = '/'; + } + }, 1000); + } + + } + } diff --git a/src/app/interfaces/settings.ts b/src/app/interfaces/settings.ts index da4d846..6fed453 100644 --- a/src/app/interfaces/settings.ts +++ b/src/app/interfaces/settings.ts @@ -5,4 +5,5 @@ export interface Settings { showTaskCount?: boolean; showNotifications?: boolean; showProgressBar?: boolean; + initialChoice?: string; } diff --git a/src/assets/images/offline-svgrepo-com.svg b/src/assets/images/offline-svgrepo-com.svg new file mode 100644 index 0000000..992fee3 --- /dev/null +++ b/src/assets/images/offline-svgrepo-com.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/assets/images/online-svgrepo-com.svg b/src/assets/images/online-svgrepo-com.svg new file mode 100644 index 0000000..a8e81d8 --- /dev/null +++ b/src/assets/images/online-svgrepo-com.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file From b377db9d3ffa748934967f371411d651512a806a Mon Sep 17 00:00:00 2001 From: iptoux Date: Sat, 19 Apr 2025 22:10:17 +0200 Subject: [PATCH 03/24] Add tasks page and update app routing structure Introduced a new tasks page with related components, styling, and tests. Updated app routes to include the tasks page and adjusted the app layout to use `router-outlet` for dynamic page rendering. Slightly modified global styles for better layout consistency. --- src/app/app.component.html | 28 +++++++----------- src/app/app.component.ts | 29 +++++++++---------- src/app/app.routes.ts | 9 +++++- .../components/page/tasks/tasks.component.css | 0 .../page/tasks/tasks.component.html | 10 +++++++ .../page/tasks/tasks.component.spec.ts | 23 +++++++++++++++ .../components/page/tasks/tasks.component.ts | 21 ++++++++++++++ src/styles.css | 12 ++++++++ 8 files changed, 99 insertions(+), 33 deletions(-) create mode 100644 src/app/components/page/tasks/tasks.component.css create mode 100644 src/app/components/page/tasks/tasks.component.html create mode 100644 src/app/components/page/tasks/tasks.component.spec.ts create mode 100644 src/app/components/page/tasks/tasks.component.ts diff --git a/src/app/app.component.html b/src/app/app.component.html index d26f0a4..4aae6f0 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,19 +1,13 @@ - -
- - @if(showNotifications) { - - } - - - - -
- +
+ + +
+ + @if(showNotifications) { + + } +
-
- - -
- + +
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 16a3b79..a371aa2 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,43 +1,42 @@ -import {Component, inject} from '@angular/core'; +import {Component, inject, OnInit} from '@angular/core'; import {HeaderComponent} from './components/header/header.component'; -import {FilterControlsComponent} from './components/filter-controls/filter-controls.component'; -import {TaskAddComponent} from './components/task-add/task-add.component'; -import {TaskListComponent} from './components/task-list/task-list.component'; -import {TodoProgressbarComponent} from './components/todo-progressbar/todo-progressbar.component'; import {FooterComponent} from './components/footer/footer.component'; import {AnnouncementBoxComponent} from './components/announcement-box/announcement-box.component'; import {DarkModeService} from './services/dark-mode.service'; import {SettingsService} from './services/settings.service'; import {NotificationBoxComponent} from './components/notification-box/notification-box.component'; +import {InitialChoiceComponent} from './components/initial-choice/initial-choice.component'; +import {Router, RouterOutlet} from '@angular/router'; @Component({ selector: 'app-root', - imports: [HeaderComponent, FilterControlsComponent, TaskAddComponent, TaskListComponent, TodoProgressbarComponent, FooterComponent, AnnouncementBoxComponent, NotificationBoxComponent], + imports: [HeaderComponent,FooterComponent, AnnouncementBoxComponent, NotificationBoxComponent, RouterOutlet], templateUrl: './app.component.html', styleUrl: './app.component.css' }) -export class AppComponent { +export class AppComponent implements OnInit { title = 'untitled1'; private settingsService = inject(SettingsService) protected settings = this.settingsService.getSettings(); + private initialChoice = this.settings()[0]?.initialChoice; + get isDarkMode(): boolean { return this.darkModeService.isDarkMode(); } - get showProgressBar():boolean { - return this.settings()[0]?.showProgressBar || false - } - get showNotifications():boolean { return this.settings()[0]?.showNotifications || false } - constructor(private darkModeService: DarkModeService) {} + constructor( + private darkModeService: DarkModeService, + private router: Router) {} - toggleDarkMode(): void { - this.darkModeService.toggleDarkMode(); + ngOnInit() { + if(this.initialChoice === undefined) { + void this.router.navigate(['/initial-choice']); + } } - } diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 235192a..d50e077 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,5 +1,12 @@ import { Routes } from '@angular/router'; +import {TasksComponent} from './components/page/tasks/tasks.component'; +import {InitialChoiceComponent} from './components/initial-choice/initial-choice.component'; +import {CreateAccountComponent} from './components/account/create-account/create-account.component'; export const routes: Routes = [ - { path: '', redirectTo: 'index.html', pathMatch: 'full' }, + { path: '', redirectTo: 'tasks', pathMatch: 'full' }, + { path: 'tasks', component: TasksComponent }, + { path: 'initial-choice', component: InitialChoiceComponent}, + { path: 'account/create/:action', component: CreateAccountComponent }, + { path: '**', redirectTo: 'index.html' } ]; diff --git a/src/app/components/page/tasks/tasks.component.css b/src/app/components/page/tasks/tasks.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/page/tasks/tasks.component.html b/src/app/components/page/tasks/tasks.component.html new file mode 100644 index 0000000..a5e5bfa --- /dev/null +++ b/src/app/components/page/tasks/tasks.component.html @@ -0,0 +1,10 @@ + + + + +
+ +
+
+ + diff --git a/src/app/components/page/tasks/tasks.component.spec.ts b/src/app/components/page/tasks/tasks.component.spec.ts new file mode 100644 index 0000000..220a8e5 --- /dev/null +++ b/src/app/components/page/tasks/tasks.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TasksComponent } from './tasks.component'; + +describe('TasksComponent', () => { + let component: TasksComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TasksComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TasksComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/page/tasks/tasks.component.ts b/src/app/components/page/tasks/tasks.component.ts new file mode 100644 index 0000000..bbe2248 --- /dev/null +++ b/src/app/components/page/tasks/tasks.component.ts @@ -0,0 +1,21 @@ +import {Component, inject} from '@angular/core'; +import {FilterControlsComponent} from '../../filter-controls/filter-controls.component'; +import {TaskAddComponent} from '../../task-add/task-add.component'; +import {TodoProgressbarComponent} from '../../todo-progressbar/todo-progressbar.component'; +import {TaskListComponent} from '../../task-list/task-list.component'; +import {SettingsService} from '../../../services/settings.service'; + +@Component({ + selector: 'app-tasks', + imports: [FilterControlsComponent, TaskAddComponent, TodoProgressbarComponent, TaskListComponent], + templateUrl: './tasks.component.html', + styleUrl: './tasks.component.css' +}) +export class TasksComponent { + private settingsService = inject(SettingsService) + protected settings = this.settingsService.getSettings(); + + get showProgressBar():boolean { + return this.settings()[0]?.showProgressBar || false + } +} diff --git a/src/styles.css b/src/styles.css index 415391d..d808f7a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -127,3 +127,15 @@ li > .close-btn:hover { left: 100%; } } + + +.page-container { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.content { + flex: 1 0 auto; + padding-bottom: 60px; /* Adjust based on your footer height */ +} From 45a461767d290862e5c7a50653b986fd75e11ec5 Mon Sep 17 00:00:00 2001 From: iptoux Date: Sat, 19 Apr 2025 22:10:27 +0200 Subject: [PATCH 04/24] Add CreateAccountComponent with basic structure and logic Introduced a new CreateAccountComponent, including HTML template, CSS, TypeScript logic, and unit test. The component handles different modes ('local' and 'online') based on route parameters, initializing forms accordingly. --- .../create-account.component.css | 0 .../create-account.component.html | 1 + .../create-account.component.spec.ts | 23 ++++++++++++ .../create-account.component.ts | 37 +++++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 src/app/components/account/create-account/create-account.component.css create mode 100644 src/app/components/account/create-account/create-account.component.html create mode 100644 src/app/components/account/create-account/create-account.component.spec.ts create mode 100644 src/app/components/account/create-account/create-account.component.ts diff --git a/src/app/components/account/create-account/create-account.component.css b/src/app/components/account/create-account/create-account.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/account/create-account/create-account.component.html b/src/app/components/account/create-account/create-account.component.html new file mode 100644 index 0000000..38e2684 --- /dev/null +++ b/src/app/components/account/create-account/create-account.component.html @@ -0,0 +1 @@ +

create-account works!

diff --git a/src/app/components/account/create-account/create-account.component.spec.ts b/src/app/components/account/create-account/create-account.component.spec.ts new file mode 100644 index 0000000..b473c88 --- /dev/null +++ b/src/app/components/account/create-account/create-account.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CreateAccountComponent } from './create-account.component'; + +describe('CreateAccountComponent', () => { + let component: CreateAccountComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CreateAccountComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CreateAccountComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/account/create-account/create-account.component.ts b/src/app/components/account/create-account/create-account.component.ts new file mode 100644 index 0000000..f332961 --- /dev/null +++ b/src/app/components/account/create-account/create-account.component.ts @@ -0,0 +1,37 @@ +import {Component, OnInit} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; + +@Component({ + selector: 'app-create-account', + imports: [], + templateUrl: './create-account.component.html', + styleUrl: './create-account.component.css' +}) +export class CreateAccountComponent implements OnInit { + action:string = ''; + + constructor(private route: ActivatedRoute) {} + + ngOnInit(): void { + this.route.params.subscribe(params => { + this.action = params['action']; + this.handleAction(); + }) + } + + private handleAction() { + switch (this.action) { + case 'local': + console.log('Account creation mode'); + // Initialize form for creation + break; + case 'online': + console.log('Account editing mode'); + // Load account data and initialize form for editing + break; + default: + console.log('Unknown action:', this.action); + // Handle unknown action + } + } +} From 1b189e862eb3b37fd91f265f96eec90ea6b71858 Mon Sep 17 00:00:00 2001 From: iptoux Date: Sat, 19 Apr 2025 22:10:45 +0200 Subject: [PATCH 05/24] Remove unused InitialChoiceComponent import The InitialChoiceComponent import was no longer in use and has been removed to clean up the codebase. This helps improve clarity and reduces unnecessary dependencies. --- src/app/app.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index a371aa2..31fbe56 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -5,7 +5,6 @@ import {AnnouncementBoxComponent} from './components/announcement-box/announceme import {DarkModeService} from './services/dark-mode.service'; import {SettingsService} from './services/settings.service'; import {NotificationBoxComponent} from './components/notification-box/notification-box.component'; -import {InitialChoiceComponent} from './components/initial-choice/initial-choice.component'; import {Router, RouterOutlet} from '@angular/router'; @Component({ From d896c2d1606efc2950081f1a398ea664dd6e436b Mon Sep 17 00:00:00 2001 From: iptoux Date: Sat, 19 Apr 2025 22:45:33 +0200 Subject: [PATCH 06/24] Revamp create-account UI with form and dark mode support Replaced placeholder text with a functional UI for account creation, including username, password, and avatar inputs. Integrated dark mode styling through a service and added responsive CSS adjustments. Improved component modularity with updated imports and reactive capabilities. --- .../create-account.component.css | 62 +++++++++++++++++++ .../create-account.component.html | 51 ++++++++++++++- .../create-account.component.ts | 19 +++++- 3 files changed, 128 insertions(+), 4 deletions(-) diff --git a/src/app/components/account/create-account/create-account.component.css b/src/app/components/account/create-account/create-account.component.css index e69de29..861e0b4 100644 --- a/src/app/components/account/create-account/create-account.component.css +++ b/src/app/components/account/create-account/create-account.component.css @@ -0,0 +1,62 @@ +.card { + border-radius: 0.15rem; +} + +form > *, form > * > * > * { + border-radius: 0; +} + +form > button { + background-color: #4c8bf5; + border: 0; + color: #fff; +} + +form > button:hover { + background-color: #9463f7; + color: rgba(255, 255, 255, 0.85); +} + + +/* Dark theme styles for task-add component */ +.dark-theme { + background-color: var(--surface-color); + border-color: var(--border-color); + color: var(--text-color); +} + +/* Style for input fields in dark mode */ +.dark-theme .form-control { + background-color: #1a1a2e; /* Dark background for input */ + color: var(--text-color); /* White text */ + border-color: var(--border-color); +} + +/* Style for placeholder text in dark mode */ +.dark-theme .form-control::placeholder { + color: rgba(255, 255, 255, 0.6); /* Slightly dimmed for placeholder */ +} + +/* Style for focused input in dark mode */ +.dark-theme .form-control:focus { + background-color: #2a2a3e; /* Slightly lighter when focused */ + box-shadow: 0 0 0 0.25rem rgba(187, 134, 252, 0.25); /* Glow that matches primary color */ +} + +/* Dark theme specific styles for file input */ +.dark-theme input[type="file"].form-control { + background-color: #1a1a2e; + color: var(--text-color); +} + +.dark-theme input[type="file"].form-control::file-selector-button { + background-color: #333345; + color: white; +} + +.dark-theme input[type="file"].form-control::file-selector-button:hover { + background-color: #4c4c6d; +} + + + diff --git a/src/app/components/account/create-account/create-account.component.html b/src/app/components/account/create-account/create-account.component.html index 38e2684..0508713 100644 --- a/src/app/components/account/create-account/create-account.component.html +++ b/src/app/components/account/create-account/create-account.component.html @@ -1 +1,50 @@ -

create-account works!

+
+
+

Local Account Setup

+

+
+ +
+
+ Setup your local account +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+
+
diff --git a/src/app/components/account/create-account/create-account.component.ts b/src/app/components/account/create-account/create-account.component.ts index f332961..57c6a4f 100644 --- a/src/app/components/account/create-account/create-account.component.ts +++ b/src/app/components/account/create-account/create-account.component.ts @@ -1,16 +1,29 @@ -import {Component, OnInit} from '@angular/core'; +import {Component, inject, OnInit} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; +import {DarkModeService} from '../../../services/dark-mode.service'; +import {NgClass} from '@angular/common'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; @Component({ selector: 'app-create-account', - imports: [], + imports: [ + NgClass, + FormsModule, + ReactiveFormsModule + ], templateUrl: './create-account.component.html', styleUrl: './create-account.component.css' }) export class CreateAccountComponent implements OnInit { action:string = ''; - constructor(private route: ActivatedRoute) {} + constructor(private route: ActivatedRoute, + private darkModeService: DarkModeService,) {} + + get isDarkMode(): boolean { + return this.darkModeService.isDarkMode(); + } + ngOnInit(): void { this.route.params.subscribe(params => { From ba3327f026db9c93778abe4325e9a1c3e26fa8cc Mon Sep 17 00:00:00 2001 From: iptoux Date: Sat, 19 Apr 2025 22:46:33 +0200 Subject: [PATCH 07/24] Remove unused 'inject' import from create-account component. The 'inject' import was not being used in the create-account component and has been removed to clean up the code. This improves readability and reduces unnecessary imports. --- .../account/create-account/create-account.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/account/create-account/create-account.component.ts b/src/app/components/account/create-account/create-account.component.ts index 57c6a4f..fe2861a 100644 --- a/src/app/components/account/create-account/create-account.component.ts +++ b/src/app/components/account/create-account/create-account.component.ts @@ -1,4 +1,4 @@ -import {Component, inject, OnInit} from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {DarkModeService} from '../../../services/dark-mode.service'; import {NgClass} from '@angular/common'; From 91cb16f24297ae5a3b5827d4db0669bebf69defa Mon Sep 17 00:00:00 2001 From: iptoux Date: Sat, 19 Apr 2025 23:15:04 +0200 Subject: [PATCH 08/24] Add avatar and enhanced instructions to account creation Updated the UI to include avatar input and improved setup instructions for clarity. Added functionality in the component to handle avatar during account creation using the AccountService. --- .../create-account/create-account.component.html | 8 ++++++-- .../create-account/create-account.component.ts | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/app/components/account/create-account/create-account.component.html b/src/app/components/account/create-account/create-account.component.html index 0508713..acb18d3 100644 --- a/src/app/components/account/create-account/create-account.component.html +++ b/src/app/components/account/create-account/create-account.component.html @@ -1,7 +1,11 @@

Local Account Setup

-

+

+ To create your local account, simply enter a username, choose a password (which will encrypt all your data), and + upload an avatar of your choice. Be sure to remember this password—if you ever export your data and import it on + another device, you’ll need the exact same secret to restore access. +

@@ -42,7 +46,7 @@

Local Account Setup

- +
diff --git a/src/app/components/account/create-account/create-account.component.ts b/src/app/components/account/create-account/create-account.component.ts index fe2861a..b732296 100644 --- a/src/app/components/account/create-account/create-account.component.ts +++ b/src/app/components/account/create-account/create-account.component.ts @@ -3,6 +3,8 @@ import {ActivatedRoute} from '@angular/router'; import {DarkModeService} from '../../../services/dark-mode.service'; import {NgClass} from '@angular/common'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {AccountService} from '../../../services/account.service'; +import {Account} from '../../../interfaces/account'; @Component({ selector: 'app-create-account', @@ -18,7 +20,8 @@ export class CreateAccountComponent implements OnInit { action:string = ''; constructor(private route: ActivatedRoute, - private darkModeService: DarkModeService,) {} + private darkModeService: DarkModeService, + private accountService: AccountService) {} get isDarkMode(): boolean { return this.darkModeService.isDarkMode(); @@ -32,6 +35,16 @@ export class CreateAccountComponent implements OnInit { }) } + createAccount(name:string, password:string,avatar:string) { + const account:Account = { + offline: true, + username: name, + localStorageKey: password, + avatar: avatar, + } + this.accountService.createAccount(account); + } + private handleAction() { switch (this.action) { case 'local': From fae2c7dc7474e153719cf71133c51ecfb9ef0e64 Mon Sep 17 00:00:00 2001 From: iptoux Date: Sat, 19 Apr 2025 23:15:14 +0200 Subject: [PATCH 09/24] Add AccountService with basic methods and unit test Introduced `AccountService` to handle account creation. Implemented methods for creating local and remote accounts, with logic branching based on account type. Added a unit test to ensure the service is created successfully. --- src/app/interfaces/account.ts | 8 ++++++ src/app/services/account.service.spec.ts | 16 ++++++++++++ src/app/services/account.service.ts | 32 ++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 src/app/interfaces/account.ts create mode 100644 src/app/services/account.service.spec.ts create mode 100644 src/app/services/account.service.ts diff --git a/src/app/interfaces/account.ts b/src/app/interfaces/account.ts new file mode 100644 index 0000000..c9dc37a --- /dev/null +++ b/src/app/interfaces/account.ts @@ -0,0 +1,8 @@ +export interface Account { + offline: boolean; + username: string; + password?: string; + localStorageKey?: string; + email?: string; + avatar?: string; +} diff --git a/src/app/services/account.service.spec.ts b/src/app/services/account.service.spec.ts new file mode 100644 index 0000000..2ffad6f --- /dev/null +++ b/src/app/services/account.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AccountService } from './account.service'; + +describe('AccountService', () => { + let service: AccountService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AccountService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/account.service.ts b/src/app/services/account.service.ts new file mode 100644 index 0000000..16965eb --- /dev/null +++ b/src/app/services/account.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import {Account} from '../interfaces/account'; + +@Injectable({ + providedIn: 'root' +}) +export class AccountService { + + private account: Account|undefined; + + constructor() { } + + createAccount(account: Account) { + this.account = account; + if (!account.offline) { + this.createRemoteAccount(); + } else { + this.createLocalAccount(); + } + } + + private createLocalAccount() { + console.log('Creating local account'); + console.log(this.account); + } + + private createRemoteAccount() { + + } + + +} From 3178f64818d8047d8db0eb93c4b0437c68fec8a0 Mon Sep 17 00:00:00 2001 From: iptoux Date: Sun, 20 Apr 2025 00:15:53 +0200 Subject: [PATCH 10/24] Refactor account creation with reactive forms and validations Replaced template-driven forms with reactive forms for better validation and state management in account creation. Introduced offline/online account distinction and added validation messages and file handling for avatars. Enhanced the account service to notify users and route upon successful account creation. --- .../create-account.component.html | 63 ++++++++++------ .../create-account.component.ts | 75 ++++++++++++++++--- src/app/components/footer/footer.component.ts | 1 + .../initial-choice.component.ts | 23 +++--- src/app/services/account.service.ts | 16 ++-- 5 files changed, 129 insertions(+), 49 deletions(-) diff --git a/src/app/components/account/create-account/create-account.component.html b/src/app/components/account/create-account/create-account.component.html index acb18d3..50cd7a0 100644 --- a/src/app/components/account/create-account/create-account.component.html +++ b/src/app/components/account/create-account/create-account.component.html @@ -13,40 +13,59 @@

Local Account Setup

Setup your local account
-
+
- + + @if (accountForm.get('username')?.invalid && (accountForm.get('username')?.dirty || accountForm.get('username')?.touched)) { +
+ Username is required +
+ } +
- + + @if (accountForm.get('password')?.invalid && (accountForm.get('password')?.dirty || accountForm.get('password')?.touched)) { +
+ Password is required +
+ } +
- + + @if (!selectedFile && accountForm.touched) { +
+ Avatar is required +
+ } +
- +
diff --git a/src/app/components/account/create-account/create-account.component.ts b/src/app/components/account/create-account/create-account.component.ts index b732296..83eac54 100644 --- a/src/app/components/account/create-account/create-account.component.ts +++ b/src/app/components/account/create-account/create-account.component.ts @@ -2,7 +2,7 @@ import {Component, OnInit} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {DarkModeService} from '../../../services/dark-mode.service'; import {NgClass} from '@angular/common'; -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms'; import {AccountService} from '../../../services/account.service'; import {Account} from '../../../interfaces/account'; @@ -18,16 +18,63 @@ import {Account} from '../../../interfaces/account'; }) export class CreateAccountComponent implements OnInit { action:string = ''; + offline: boolean = false; + accountForm: FormGroup; + selectedFile: File | null = null; - constructor(private route: ActivatedRoute, + private localAccount: Account = { + offline: true, + username: '' + }; + + private onlineAccount: Account = { + offline: false, + username: '' + }; + + constructor(private fb: FormBuilder, + private route: ActivatedRoute, private darkModeService: DarkModeService, - private accountService: AccountService) {} + private accountService: AccountService) + { + this.accountForm = this.fb.group({ + username: ['', Validators.required], + password: ['', Validators.required], + }); + + } get isDarkMode(): boolean { return this.darkModeService.isDarkMode(); } + onFileSelected(event: any) { + if (event.target.files.length > 0) { + this.selectedFile = event.target.files[0]; + } + } + + submitForm() { + if (this.accountForm.valid && this.selectedFile) { + const reader = new FileReader(); + reader.onload = (e) => { + const avatar = e.target?.result as string; + this.createAccount( + this.accountForm.get('username')?.value, + this.accountForm.get('password')?.value, + this.accountForm.get('email')?.value.replace('@', '_at_').replace('.', '_dot_').replace(' ', '_'), + avatar + ); + }; + reader.readAsDataURL(this.selectedFile); + } else { + // Mark all fields as touched to trigger validation messages + this.accountForm.markAllAsTouched(); + } + } + + ngOnInit(): void { this.route.params.subscribe(params => { this.action = params['action']; @@ -35,20 +82,28 @@ export class CreateAccountComponent implements OnInit { }) } - createAccount(name:string, password:string,avatar:string) { - const account:Account = { - offline: true, - username: name, - localStorageKey: password, - avatar: avatar, + createAccount(name: string, password: string, email: string, avatar: string) { + if(this.offline) { + this.localAccount.offline = true; + this.localAccount.username = name; + this.localAccount.localStorageKey = password; + this.localAccount.avatar = avatar; + this.accountService.createAccount(this.localAccount); + } else { + this.onlineAccount.offline = false; + this.onlineAccount.username = name; + this.onlineAccount.email = email; + this.onlineAccount.password = password; + this.onlineAccount.avatar = avatar; + this.accountService.createAccount(this.onlineAccount); } - this.accountService.createAccount(account); } private handleAction() { switch (this.action) { case 'local': console.log('Account creation mode'); + this.offline = true; // Initialize form for creation break; case 'online': diff --git a/src/app/components/footer/footer.component.ts b/src/app/components/footer/footer.component.ts index f128036..c2dfc1e 100644 --- a/src/app/components/footer/footer.component.ts +++ b/src/app/components/footer/footer.component.ts @@ -156,6 +156,7 @@ export class FooterComponent implements OnInit { this.tasksService.clearAllTasks(); localStorage.removeItem('AGTASKS_SETTINGS') localStorage.removeItem('AGTASKS_ANNOUNCEMENTS') + localStorage.removeItem('AGTASKS_ACCOUNT') this.notificationService.addNotification( 'Success!', 'All your user data has been deleted.') diff --git a/src/app/components/initial-choice/initial-choice.component.ts b/src/app/components/initial-choice/initial-choice.component.ts index a4403e7..85f4ab1 100644 --- a/src/app/components/initial-choice/initial-choice.component.ts +++ b/src/app/components/initial-choice/initial-choice.component.ts @@ -51,10 +51,11 @@ export class InitialChoiceComponent { return setting; }); - // Update the settings with the new array - this.settingsService.updateSettings(updatedSettings); + console.log("Initial choice set to: " + choice); if (choice === 'offline') { + // Update the settings with the new array + this.settingsService.updateSettings(updatedSettings); // Redirect to home void this.router.navigate(['/account/create/local']); } else if(choice === 'online') { @@ -62,15 +63,15 @@ export class InitialChoiceComponent { this.notificationService.addNotification("Info:", "Online mode not implemented yet."); // sleep and show countdown 5 sec - this.showCountdown = true; - const intervalId = setInterval(() => { - this.countdown--; - - if (this.countdown <= 0) { - clearInterval(intervalId); - window.location.href = '/'; - } - }, 1000); + // this.showCountdown = true; + // const intervalId = setInterval(() => { + // this.countdown--; + // + // if (this.countdown <= 0) { + // clearInterval(intervalId); + // window.location.href = '/'; + // } + // }, 1000); } } diff --git a/src/app/services/account.service.ts b/src/app/services/account.service.ts index 16965eb..aab59f2 100644 --- a/src/app/services/account.service.ts +++ b/src/app/services/account.service.ts @@ -1,5 +1,7 @@ -import { Injectable } from '@angular/core'; +import {inject, Injectable} from '@angular/core'; import {Account} from '../interfaces/account'; +import {NotificationService} from './notification.service'; +import {Router} from '@angular/router'; @Injectable({ providedIn: 'root' @@ -7,8 +9,9 @@ import {Account} from '../interfaces/account'; export class AccountService { private account: Account|undefined; + private notificationService = inject(NotificationService); - constructor() { } + constructor(private router: Router) { } createAccount(account: Account) { this.account = account; @@ -20,13 +23,14 @@ export class AccountService { } private createLocalAccount() { - console.log('Creating local account'); - console.log(this.account); + localStorage.setItem('AGTASKS_ACCOUNT', JSON.stringify(this.account)); + this.notificationService.addNotification('Success', 'Account created successfully.'); + void this.router.navigate(['/']); } private createRemoteAccount() { - + console.log('Creating remote account'); + console.log('Not implemented yet.'); } - } From f50767d9cda8d6d8fd09c6a5063916bdaf900ae3 Mon Sep 17 00:00:00 2001 From: iptoux Date: Sun, 20 Apr 2025 00:18:15 +0200 Subject: [PATCH 11/24] Update form validation to use 'dirty' instead of 'touched' Switched from 'touched' to 'dirty' in the account creation form to improve validation accuracy. This ensures error messages are displayed only when the user interacts and modifies the input field. --- .../account/create-account/create-account.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/account/create-account/create-account.component.html b/src/app/components/account/create-account/create-account.component.html index 50cd7a0..4356f4a 100644 --- a/src/app/components/account/create-account/create-account.component.html +++ b/src/app/components/account/create-account/create-account.component.html @@ -56,7 +56,7 @@

Local Account Setup

class="form-control" placeholder="Choose your avatar..." autocomplete="photo" /> - @if (!selectedFile && accountForm.touched) { + @if (!selectedFile && accountForm.dirty) {
Avatar is required
From 368fe773ba0f5a945e77081c3d8246d696abc746 Mon Sep 17 00:00:00 2001 From: iptoux Date: Sun, 20 Apr 2025 01:03:22 +0200 Subject: [PATCH 12/24] Add image resizing to account creation process Implemented a method to resize avatars to 128x128 pixels for consistency during account creation. Added error handling for resizing failures and introduced a `loadLocalAccount` method to retrieve stored account data from local storage. --- src/app/services/account.service.ts | 59 ++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/app/services/account.service.ts b/src/app/services/account.service.ts index aab59f2..eca30a5 100644 --- a/src/app/services/account.service.ts +++ b/src/app/services/account.service.ts @@ -13,7 +13,58 @@ export class AccountService { constructor(private router: Router) { } - createAccount(account: Account) { + /** + * Resizes an image to 128x128 pixels and returns it as a data URL + * @param imageDataUrl The original image data URL + * @returns Promise with the resized image data URL + */ + resizeAvatar(imageDataUrl: string): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + // Create a canvas element to resize the image + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + // Set target dimensions + canvas.width = 128; + canvas.height = 128; + + if (ctx) { + // Draw the image with the new dimensions + ctx.drawImage(img, 0, 0, 128, 128); + + // Get the resized image as a data URL (same type as original) + const format = imageDataUrl.split(';')[0].split(':')[1] || 'image/jpeg'; + const resizedDataUrl = canvas.toDataURL(format, 0.9); // 0.9 quality + resolve(resizedDataUrl); + } else { + reject(new Error('Could not get canvas context')); + } + }; + + img.onerror = () => { + reject(new Error('Failed to load image')); + }; + + // Set the source of the image to the data URL + img.src = imageDataUrl; + }); + } + + + + async createAccount(account: Account) { + // If there's an avatar, resize it + if (account.avatar) { + try { + account.avatar = await this.resizeAvatar(account.avatar); + } catch (error) { + console.error('Failed to resize avatar:', error); + this.notificationService.addNotification('Warning', 'Failed to resize avatar. Using original image.'); + } + } + this.account = account; if (!account.offline) { this.createRemoteAccount(); @@ -22,15 +73,21 @@ export class AccountService { } } + private createLocalAccount() { localStorage.setItem('AGTASKS_ACCOUNT', JSON.stringify(this.account)); this.notificationService.addNotification('Success', 'Account created successfully.'); void this.router.navigate(['/']); } + private createRemoteAccount() { console.log('Creating remote account'); console.log('Not implemented yet.'); } + loadLocalAccount(): Account|undefined { + this.account = JSON.parse(localStorage.getItem('AGTASKS_ACCOUNT') || '{}'); + return this.account; + } } From 19e90fa1bec911c5352ee036daf26c3393848bc0 Mon Sep 17 00:00:00 2001 From: iptoux Date: Sun, 20 Apr 2025 01:03:44 +0200 Subject: [PATCH 13/24] Add user account handling and pass to footer component Introduced `Account` interface and `AccountService` to manage user account data. Now loading the local user account if the initial choice is offline and passing the account information to the footer component via input binding. This improves user-specific functionality and prepares the app for account-aware features. --- src/app/app.component.html | 2 +- src/app/app.component.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index 4aae6f0..6142220 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -9,5 +9,5 @@ - + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 31fbe56..280fcb9 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -6,6 +6,8 @@ import {DarkModeService} from './services/dark-mode.service'; import {SettingsService} from './services/settings.service'; import {NotificationBoxComponent} from './components/notification-box/notification-box.component'; import {Router, RouterOutlet} from '@angular/router'; +import {Account} from './interfaces/account'; +import {AccountService} from './services/account.service'; @Component({ selector: 'app-root', @@ -15,6 +17,7 @@ import {Router, RouterOutlet} from '@angular/router'; }) export class AppComponent implements OnInit { title = 'untitled1'; + useraccount: Account | undefined private settingsService = inject(SettingsService) protected settings = this.settingsService.getSettings(); @@ -31,11 +34,14 @@ export class AppComponent implements OnInit { constructor( private darkModeService: DarkModeService, - private router: Router) {} + private router: Router, + private accountService: AccountService) {} ngOnInit() { if(this.initialChoice === undefined) { void this.router.navigate(['/initial-choice']); - } + } else + if(this.settings()[0]?.initialChoice === 'offline') + this.useraccount = this.accountService.loadLocalAccount() } } From ffe7e82302627bfc940626672d64b07b7c946912 Mon Sep 17 00:00:00 2001 From: iptoux Date: Sun, 20 Apr 2025 01:03:52 +0200 Subject: [PATCH 14/24] Update footer to display user avatar and logout tooltip Replaced static icons with dynamic user avatar and logout functionality in the footer. Added logic to handle avatar retrieval and decoding, with a fallback to a default avatar if necessary. Enhanced tooltip to display the username for better user context. --- .../components/footer/footer.component.html | 17 +++---- src/app/components/footer/footer.component.ts | 48 ++++++++++++++++++- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/src/app/components/footer/footer.component.html b/src/app/components/footer/footer.component.html index 571effe..33d07a8 100644 --- a/src/app/components/footer/footer.component.html +++ b/src/app/components/footer/footer.component.html @@ -7,19 +7,20 @@
- - + logo +
): void { const currentSettings = this.settingsService.getSettings()(); if (currentSettings && currentSettings.length > 0) { From ed6f6ba3fd2e76fa07512e68aa2af32879d0c9b6 Mon Sep 17 00:00:00 2001 From: iptoux Date: Sun, 20 Apr 2025 01:04:09 +0200 Subject: [PATCH 15/24] Remove unused NgOptimizedImage import from footer component. The NgOptimizedImage import was unnecessary and has been removed to clean up unused code. This improves maintainability and avoids potential confusion in the future. --- src/app/components/footer/footer.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/footer/footer.component.ts b/src/app/components/footer/footer.component.ts index 019f494..62b2bd8 100644 --- a/src/app/components/footer/footer.component.ts +++ b/src/app/components/footer/footer.component.ts @@ -5,7 +5,7 @@ import {Announcement} from '../../interfaces/announcement'; import {FormsModule} from '@angular/forms'; import {SettingsService} from '../../services/settings.service'; import {Settings} from '../../interfaces/settings' -import {NgClass, NgOptimizedImage} from '@angular/common'; +import {NgClass} from '@angular/common'; import {DarkModeService} from '../../services/dark-mode.service'; import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap'; import {ModalService} from '../../services/modal.service'; From 4d4946473180f14dcb7e0f7d03895dadb75cd85c Mon Sep 17 00:00:00 2001 From: iptoux Date: Sun, 20 Apr 2025 01:12:31 +0200 Subject: [PATCH 16/24] Remove logout icon and switch default avatar to SVG Commented out the logout icon in the footer for a cleaner design and replaced the default avatar image from PNG to a new SVG file. This ensures improved scalability and visual quality for the default avatar. --- src/app/components/footer/footer.component.html | 12 ++++++------ src/app/components/footer/footer.component.ts | 2 +- src/assets/default-avatar.svg | 4 ++++ 3 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 src/assets/default-avatar.svg diff --git a/src/app/components/footer/footer.component.html b/src/app/components/footer/footer.component.html index 33d07a8..58ae1af 100644 --- a/src/app/components/footer/footer.component.html +++ b/src/app/components/footer/footer.component.html @@ -7,12 +7,12 @@
- + + + + + + logo + + + From 105bc0f0ff9c37ec4dc46861ec54f8e8de5357c3 Mon Sep 17 00:00:00 2001 From: iptoux Date: Sun, 20 Apr 2025 01:35:50 +0200 Subject: [PATCH 17/24] Add EventEmitter to notify account changes in AccountService Implemented an `accountChanged` EventEmitter in AccountService to emit updates when the account changes or is loaded from local storage. Updated AppComponent to subscribe to these changes and synchronize the user account accordingly. --- src/app/app.component.ts | 7 ++++++- src/app/services/account.service.ts | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 280fcb9..c505e13 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -35,7 +35,12 @@ export class AppComponent implements OnInit { constructor( private darkModeService: DarkModeService, private router: Router, - private accountService: AccountService) {} + private accountService: AccountService) { + this.accountService.accountChanged.subscribe(account => { + this.useraccount = account; + }); + + } ngOnInit() { if(this.initialChoice === undefined) { diff --git a/src/app/services/account.service.ts b/src/app/services/account.service.ts index eca30a5..4eb6578 100644 --- a/src/app/services/account.service.ts +++ b/src/app/services/account.service.ts @@ -1,4 +1,4 @@ -import {inject, Injectable} from '@angular/core'; +import {EventEmitter, inject, Injectable} from '@angular/core'; import {Account} from '../interfaces/account'; import {NotificationService} from './notification.service'; import {Router} from '@angular/router'; @@ -9,6 +9,7 @@ import {Router} from '@angular/router'; export class AccountService { private account: Account|undefined; + public accountChanged = new EventEmitter(); private notificationService = inject(NotificationService); constructor(private router: Router) { } @@ -71,6 +72,8 @@ export class AccountService { } else { this.createLocalAccount(); } + + this.accountChanged.emit(this.account); } @@ -88,6 +91,7 @@ export class AccountService { loadLocalAccount(): Account|undefined { this.account = JSON.parse(localStorage.getItem('AGTASKS_ACCOUNT') || '{}'); + this.accountChanged.emit(this.account); return this.account; } } From a2ce1fda4b0b84df4a2556c3f18d5df62ff0b229 Mon Sep 17 00:00:00 2001 From: iptoux Date: Sun, 20 Apr 2025 11:25:05 +0200 Subject: [PATCH 18/24] Add getSecret method and update account creation navigation Introduced a getSecret method to retrieve the account password safely. Updated the navigation path for account creation to redirect to the '/tasks/migration' route instead of the root. These changes improve functionality and refine user flow. --- src/app/services/account.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/services/account.service.ts b/src/app/services/account.service.ts index 4eb6578..4a4b8e0 100644 --- a/src/app/services/account.service.ts +++ b/src/app/services/account.service.ts @@ -76,11 +76,14 @@ export class AccountService { this.accountChanged.emit(this.account); } + getSecret(): string { + return this.account?.password || ''; + } private createLocalAccount() { localStorage.setItem('AGTASKS_ACCOUNT', JSON.stringify(this.account)); this.notificationService.addNotification('Success', 'Account created successfully.'); - void this.router.navigate(['/']); + void this.router.navigate(['/tasks/migration']); } From d75fd123fe766839f2a97acba5ccde7dc7b0df4a Mon Sep 17 00:00:00 2001 From: iptoux Date: Sun, 20 Apr 2025 11:25:19 +0200 Subject: [PATCH 19/24] Add task migration component with encryption and progress tracking This commit introduces a new TaskMigrationComponent to handle task migrations with AES-GCM encryption for secure data storage. It includes a progress tracker, dark mode styling, and test cases for component validation. Tasks are processed sequentially with a simulated delay, and users are redirected upon completion. --- .../task-migration.component.css | 29 +++ .../task-migration.component.html | 39 ++++ .../task-migration.component.spec.ts | 23 ++ .../task-migration.component.ts | 212 ++++++++++++++++++ 4 files changed, 303 insertions(+) create mode 100644 src/app/components/task-migration/task-migration.component.css create mode 100644 src/app/components/task-migration/task-migration.component.html create mode 100644 src/app/components/task-migration/task-migration.component.spec.ts create mode 100644 src/app/components/task-migration/task-migration.component.ts diff --git a/src/app/components/task-migration/task-migration.component.css b/src/app/components/task-migration/task-migration.component.css new file mode 100644 index 0000000..e436d86 --- /dev/null +++ b/src/app/components/task-migration/task-migration.component.css @@ -0,0 +1,29 @@ +.dark-theme { + .card { + color: #fff; + .migration-desc { + margin-left: 1rem; + } + + .progress{ + border-radius: 0.15rem; + } + .progress.bg-bar-dark { + background-color: rgba(193, 127, 255, 0.3); + div { + color: #fdfdfd; + } + } + + .bg-info { + background-color: rgba(193, 127, 255, 0.15) !important; + + } + + + + } +} +.progress{ + border-radius: 0.15rem; +} diff --git a/src/app/components/task-migration/task-migration.component.html b/src/app/components/task-migration/task-migration.component.html new file mode 100644 index 0000000..7e2183a --- /dev/null +++ b/src/app/components/task-migration/task-migration.component.html @@ -0,0 +1,39 @@ +
+
+

Task Migration

+

+ Your tasks from the previous version are now being migrated—please hang tight for a moment. Once the process + completes, you’ll be whisked back to the main view and can continue using the application as usual. +

+
+
+
+
+ Loading... +
+
Migrating your tasks
+
+
+
+
+ Task: {{currentTaskDescription}} +
+
+
+ @if(progress) + { + {{ progress }}% Complete + } +
+
+
+
+
+ diff --git a/src/app/components/task-migration/task-migration.component.spec.ts b/src/app/components/task-migration/task-migration.component.spec.ts new file mode 100644 index 0000000..95e2fe4 --- /dev/null +++ b/src/app/components/task-migration/task-migration.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TaskMigrationComponent } from './task-migration.component'; + +describe('TaskMigrationComponent', () => { + let component: TaskMigrationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TaskMigrationComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TaskMigrationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/task-migration/task-migration.component.ts b/src/app/components/task-migration/task-migration.component.ts new file mode 100644 index 0000000..c431c2c --- /dev/null +++ b/src/app/components/task-migration/task-migration.component.ts @@ -0,0 +1,212 @@ +import {Component, OnInit} from '@angular/core'; +import {NgClass} from '@angular/common'; +import {DarkModeService} from '../../services/dark-mode.service'; +import {Task} from '../../interfaces/task'; +import {TasksService} from '../../services/tasks.service'; +import {Router} from '@angular/router'; +import {AccountService} from '../../services/account.service'; + +@Component({ + selector: 'app-task-migration', + imports: [ + NgClass + ], + templateUrl: './task-migration.component.html', + styleUrl: './task-migration.component.css' +}) +export class TaskMigrationComponent implements OnInit{ + + private tasks: Task[] = [] + public tasksCount: number = 0; + public currentTaskID: number = 0; + public currentTaskDescription: string = ''; + + protected secret: string|undefined; + + constructor(private darkModeService: DarkModeService, + private tasksService: TasksService, + private accountService: AccountService, + private router: Router) { + } + + loadSecret(): void { + this.secret = this.accountService.getSecret() + } + + + // Define the sleep function + sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + /** + * Encrypts a string with a given secret key + * @param text The string to encrypt + * @returns Promise with the encrypted string (base64 encoded) + */ + private async encryptTaskDesk(text: string): Promise { + + const secretKey = this.secret || ''; + + if (!text) return ''; + + try { + // Convert strings to proper format for encryption + const textEncoder = new TextEncoder(); + const encodedText = textEncoder.encode(text); + + // Create a key from the secret + const keyMaterial = await this.getKeyMaterial(secretKey); + const key = await this.deriveKey(keyMaterial); + + // Generate a random initialization vector + const iv = crypto.getRandomValues(new Uint8Array(12)); + + // Encrypt the text + const encryptedContent = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv + }, + key, + encodedText + ); + + // Combine the IV and encrypted content into a single array + const encryptedContentArray = new Uint8Array(iv.length + encryptedContent.byteLength); + encryptedContentArray.set(iv, 0); + encryptedContentArray.set(new Uint8Array(encryptedContent), iv.length); + + // Convert to Base64 string for storage + return btoa(String.fromCharCode(...encryptedContentArray)); + } catch (error) { + console.error('Encryption error:', error); + return ''; + } + } + + /** + * Decrypts an encrypted string with the same secret key used for encryption + * @param encryptedText The encrypted string (base64 encoded) + * @param secretKey The secret key for decryption + * @returns Promise with the decrypted string + */ + private async decryptTaskDesk(encryptedText: string, secretKey: string): Promise { + if (!encryptedText) return ''; + + try { + // Convert the base64 string back to array + const encryptedData = Uint8Array.from(atob(encryptedText), c => c.charCodeAt(0)); + + // Extract the IV (first 12 bytes) + const iv = encryptedData.slice(0, 12); + + // Extract the encrypted content (everything except first 12 bytes) + const encryptedContent = encryptedData.slice(12); + + // Create a key from the secret + const keyMaterial = await this.getKeyMaterial(secretKey); + const key = await this.deriveKey(keyMaterial); + + // Decrypt the content + const decryptedContent = await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv + }, + key, + encryptedContent + ); + + // Convert the decrypted content back to string + const textDecoder = new TextDecoder(); + return textDecoder.decode(decryptedContent); + } catch (error) { + console.error('Decryption error:', error); + return ''; + } + } + + + /** + * Helper function to generate key material from a password + */ + private async getKeyMaterial(password: string): Promise { + const textEncoder = new TextEncoder(); + return await crypto.subtle.importKey( + 'raw', + textEncoder.encode(password), + { name: 'PBKDF2' }, + false, + ['deriveBits', 'deriveKey'] + ); + } + + /** + * Helper function to derive an AES-GCM key from key material + */ + private async deriveKey(keyMaterial: CryptoKey): Promise { + // Use a salt (can be a fixed value as long as it's consistent) + const textEncoder = new TextEncoder(); // Fixed: Define textEncoder here + const salt = textEncoder.encode('this-is-a-fixed-salt'); + + return await crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt, + iterations: 100000, + hash: 'SHA-256' + }, + keyMaterial, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'] + ); + } + + + + migrateTasks() { + this.currentTaskID = 0; + this.loadSecret() + const processTasks = async () => { + for(let i = 0; i < this.tasks.length; i++) { + this.currentTaskID = i + 1; + this.currentTaskDescription = this.tasks[i].description || 'Processing task...'; + + // Include function to encrypt tasks with user secret + this.tasks[i].description = await this.encryptTaskDesk(this.currentTaskDescription); + this.tasksService.updateTask(this.tasks[i]); + + // Simulate task processing with a delay + await this.sleep(1000); // Adjust the time as needed for each task + } + + // Final delay after all tasks complete + await this.sleep(2500); + console.log('Migration completed'); + void this.router.navigate(['/']); + // Add your post-migration logic here + }; + + // Start the migration process + processTasks(); + + } + + + get progress(): number { + return Number(((this.currentTaskID / this.tasksCount) * 100).toFixed(0)); + } + + + get isDarkMode(): boolean { + return this.darkModeService.isDarkMode(); + } + + ngOnInit() { + this.tasks = this.tasksService.getTasks(); + this.tasksCount = this.tasks.length; + this.migrateTasks(); // Start migration when component initializes + } +} From 218a9a7a0a5f68ea35b001ed7972f366424bff17 Mon Sep 17 00:00:00 2001 From: iptoux Date: Sun, 20 Apr 2025 11:25:31 +0200 Subject: [PATCH 20/24] Add task migration route and extend Task interface Added a new 'tasks/migration' route and its associated component for handling task migrations. Extended the Task interface to include optional properties 'decryptedDescription' and 'owner' to support enhanced task details. --- src/app/app.routes.ts | 4 +++- src/app/interfaces/task.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index d50e077..347cb2c 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -2,11 +2,13 @@ import { Routes } from '@angular/router'; import {TasksComponent} from './components/page/tasks/tasks.component'; import {InitialChoiceComponent} from './components/initial-choice/initial-choice.component'; import {CreateAccountComponent} from './components/account/create-account/create-account.component'; +import {TaskMigrationComponent} from './components/task-migration/task-migration.component'; export const routes: Routes = [ { path: '', redirectTo: 'tasks', pathMatch: 'full' }, - { path: 'tasks', component: TasksComponent }, { path: 'initial-choice', component: InitialChoiceComponent}, { path: 'account/create/:action', component: CreateAccountComponent }, + { path: 'tasks', component: TasksComponent }, + { path: 'tasks/migration', component: TaskMigrationComponent }, { path: '**', redirectTo: 'index.html' } ]; diff --git a/src/app/interfaces/task.ts b/src/app/interfaces/task.ts index 4b9a547..d9bd686 100644 --- a/src/app/interfaces/task.ts +++ b/src/app/interfaces/task.ts @@ -7,8 +7,10 @@ export interface TaskOptions { export interface Task { id: number; description: string; + decryptedDescription?: string; completed: boolean; dueDate?: number; options: TaskOptions; + owner?: string; order: number; } From 7008220ac1fde924ba439ba62d25b9d00d44f63a Mon Sep 17 00:00:00 2001 From: iptoux Date: Sun, 20 Apr 2025 11:25:42 +0200 Subject: [PATCH 21/24] Decrypt task descriptions using AES-GCM. Implemented decryption logic for task descriptions to ensure they are displayed in their decrypted form. Integrated `AccountService` for retrieving the decryption key and updated the task subscription to handle decryption asynchronously. --- .../task-list/task-list.component.html | 2 +- .../task-list/task-list.component.ts | 115 ++++++++++++++---- 2 files changed, 93 insertions(+), 24 deletions(-) diff --git a/src/app/components/task-list/task-list.component.html b/src/app/components/task-list/task-list.component.html index a15dba4..f44045a 100644 --- a/src/app/components/task-list/task-list.component.html +++ b/src/app/components/task-list/task-list.component.html @@ -24,7 +24,7 @@ + [style.position]="'relative'" [style.z-index]="'1'">{{ task.decryptedDescription }}
diff --git a/src/app/components/task-list/task-list.component.ts b/src/app/components/task-list/task-list.component.ts index c58ea1a..dfb2309 100644 --- a/src/app/components/task-list/task-list.component.ts +++ b/src/app/components/task-list/task-list.component.ts @@ -9,6 +9,7 @@ import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap'; import {ClockComponent} from '../clock/clock.component'; import {ModalService} from '../../services/modal.service'; import {NotificationService} from '../../services/notification.service'; +import {AccountService} from '../../services/account.service'; @Component({ selector: 'app-task-list', @@ -35,7 +36,8 @@ export class TaskListComponent implements OnInit, OnDestroy { constructor(private tasksService: TasksService, private darkModeService: DarkModeService, - private modalService: ModalService) {} + private modalService: ModalService, + private accountService: AccountService) {} get isDarkMode(): boolean { return this.darkModeService.isDarkMode(); @@ -51,32 +53,98 @@ export class TaskListComponent implements OnInit, OnDestroy { }); } + public async decryptTask(string: string): Promise { + return await this.decryptTaskDesk(string); + } /** - * Calculates the number of hours remaining until the due date - * @param dueDate The task's due date - * @returns The number of hours left until the due date, or 0 if the due date is in the past + * Decrypts an encrypted string with the same secret key used for encryption + * @param encryptedText The encrypted string (base64 encoded) + * @returns Promise with the decrypted string */ - calculateRemainingHours(dueDate?: number): number { - if (!dueDate) { - return 0; + private async decryptTaskDesk(encryptedText: string): Promise { + const secretKey:string = this.accountService.getSecret() + + if (!encryptedText) return ''; + + try { + // Convert the base64 string back to array + const encryptedData = Uint8Array.from(atob(encryptedText), c => c.charCodeAt(0)); + + // Extract the IV (first 12 bytes) + const iv = encryptedData.slice(0, 12); + + // Extract the encrypted content (everything except first 12 bytes) + const encryptedContent = encryptedData.slice(12); + + // Create a key from the secret + const keyMaterial = await this.getKeyMaterial(secretKey); + const key = await this.deriveKey(keyMaterial); + + // Decrypt the content + const decryptedContent = await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv + }, + key, + encryptedContent + ); + + // Convert the decrypted content back to string + const textDecoder = new TextDecoder(); + return textDecoder.decode(decryptedContent); + } catch (error) { + console.error('Decryption error:', error); + return ''; } + } - const now = new Date().getTime(); - // If the due date is in the past, return 0 - if (dueDate < now) { - return 0; - } - // Calculate the total time span (in milliseconds) - const totalTimeSpan = dueDate - now; + /** + * Helper function to generate key material from a password + */ + private async getKeyMaterial(password: string): Promise { + const textEncoder = new TextEncoder(); + return await crypto.subtle.importKey( + 'raw', + textEncoder.encode(password), + { name: 'PBKDF2' }, + false, + ['deriveBits', 'deriveKey'] + ); + } - // Convert milliseconds to hours and round to the nearest integer - return Math.round(totalTimeSpan / (1000 * 60 * 60)); + /** + * Helper function to derive an AES-GCM key from key material + */ + private async deriveKey(keyMaterial: CryptoKey): Promise { + // Use a salt (can be a fixed value as long as it's consistent) + const textEncoder = new TextEncoder(); // Fixed: Define textEncoder here + const salt = textEncoder.encode('this-is-a-fixed-salt'); + + return await crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt, + iterations: 100000, + hash: 'SHA-256' + }, + keyMaterial, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'] + ); } - calculateTimeLeftPercentage(dueDate?: number): number { + + /** + * Calculates the number of hours remaining until the due date + * @param dueDate The task's due date + * @returns The number of hours left until the due date, or 0 if the due date is in the past + */ + calculateRemainingHours(dueDate?: number): number { if (!dueDate) { return 0; } @@ -91,12 +159,10 @@ export class TaskListComponent implements OnInit, OnDestroy { // Calculate the total time span (in milliseconds) const totalTimeSpan = dueDate - now; - // Limit the maximum timespan to 30 days (as in your original code) - const maxTimeSpan = 30 * 24 * 60 * 60 * 1000; - - // Calculate percentage (capped at 100%) - return Math.min(100, Math.round((totalTimeSpan / maxTimeSpan) * 100)); + // Convert milliseconds to hours and round to the nearest integer + return Math.round(totalTimeSpan / (1000 * 60 * 60)); } + /** * Checks all tasks and creates notifications for those due within one hour */ @@ -124,8 +190,11 @@ export class TaskListComponent implements OnInit, OnDestroy { ngOnInit() { - this.subscription = this.tasksService.tasks$.subscribe(tasks => { + this.subscription = this.tasksService.tasks$.subscribe(async tasks => { this.tasks = tasks; + for (const task of this.tasks) { + task.decryptedDescription = await this.decryptTaskDesk(task.description); + } this.checkTasksDueSoon(); }); } From 75b2113459daefc6d98ff20a07fe3d44201feae2 Mon Sep 17 00:00:00 2001 From: iptoux Date: Sun, 20 Apr 2025 11:26:15 +0200 Subject: [PATCH 22/24] Remove unused decryptTaskDesk method from task-migration component. The decryptTaskDesk method was no longer being used and has been removed to clean up the code. This simplifies the component and eliminates unnecessary complexity, improving maintainability. --- .../task-migration.component.ts | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/src/app/components/task-migration/task-migration.component.ts b/src/app/components/task-migration/task-migration.component.ts index c431c2c..bfddc05 100644 --- a/src/app/components/task-migration/task-migration.component.ts +++ b/src/app/components/task-migration/task-migration.component.ts @@ -85,49 +85,6 @@ export class TaskMigrationComponent implements OnInit{ } } - /** - * Decrypts an encrypted string with the same secret key used for encryption - * @param encryptedText The encrypted string (base64 encoded) - * @param secretKey The secret key for decryption - * @returns Promise with the decrypted string - */ - private async decryptTaskDesk(encryptedText: string, secretKey: string): Promise { - if (!encryptedText) return ''; - - try { - // Convert the base64 string back to array - const encryptedData = Uint8Array.from(atob(encryptedText), c => c.charCodeAt(0)); - - // Extract the IV (first 12 bytes) - const iv = encryptedData.slice(0, 12); - - // Extract the encrypted content (everything except first 12 bytes) - const encryptedContent = encryptedData.slice(12); - - // Create a key from the secret - const keyMaterial = await this.getKeyMaterial(secretKey); - const key = await this.deriveKey(keyMaterial); - - // Decrypt the content - const decryptedContent = await crypto.subtle.decrypt( - { - name: 'AES-GCM', - iv - }, - key, - encryptedContent - ); - - // Convert the decrypted content back to string - const textDecoder = new TextDecoder(); - return textDecoder.decode(decryptedContent); - } catch (error) { - console.error('Decryption error:', error); - return ''; - } - } - - /** * Helper function to generate key material from a password */ From bad43d875553df188ff2fe31c46a276ac9626094 Mon Sep 17 00:00:00 2001 From: iptoux Date: Sun, 20 Apr 2025 11:42:41 +0200 Subject: [PATCH 23/24] Refactor task display with encrypted description handling Introduce a specific `displayTasks` view-model to manage decrypted task descriptions separately from original data. This enhances separation of concerns, optimizes UI updates, and ensures better maintainability. The drag-and-drop functionality was updated to handle both the original and display arrays in sync. --- .../task-list/task-list.component.html | 2 +- .../task-list/task-list.component.ts | 37 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/app/components/task-list/task-list.component.html b/src/app/components/task-list/task-list.component.html index f44045a..8b1c5cc 100644 --- a/src/app/components/task-list/task-list.component.html +++ b/src/app/components/task-list/task-list.component.html @@ -9,7 +9,7 @@
    -
  • { this.tasks = tasks; - for (const task of this.tasks) { - task.decryptedDescription = await this.decryptTaskDesk(task.description); - } + // Anzeige-Tasks erstellen und entschlüsseln + await this.updateDisplayTasks(); + this.checkTasksDueSoon(); }); } @@ -215,11 +237,20 @@ export class TaskListComponent implements OnInit, OnDestroy { } drop(event: CdkDragDrop): void { + // Aktualisiere zuerst das Anzeige-Array für sofortige UI-Aktualisierung + moveItemInArray(this.displayTasks, event.previousIndex, event.currentIndex); + + // Dann aktualisiere das Original-Array in der gleichen Weise moveItemInArray(this.tasks, event.previousIndex, event.currentIndex); this.tasks.forEach((task, index) => { task.order = index; }); + + this.displayTasks.forEach((task, index) => { + task.order = index; + }); + this.tasksService.updateTasksOrderForFilter(this.tasks); } } From d3159d2dc4ba4a841133b5e2f1325b2dc50e2c5e Mon Sep 17 00:00:00 2001 From: iptoux Date: Sun, 20 Apr 2025 11:46:51 +0200 Subject: [PATCH 24/24] Update package version to 1.7.0 This version bump reflects changes made to the application. Ensure dependencies and functionality align with the updated version. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4edeea5..d727bd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mytaskapplication", - "version": "1.6.0", + "version": "1.7.0", "main": "app.js", "author": { "name": "Maik Roland Damm",