Project Architecture
This guide explains how Angular concepts are applied in the TimeLock project, showing you the overall architecture and design patterns used.
Table of Contents
- Application Overview
 - Architecture Patterns
 - State Management with Signals
 - Data Flow
 - Component Hierarchy
 - Service Layer
 - Routing Strategy
 - Data Persistence
 
Application Overview
TimeLock is a task management application that demonstrates modern Angular patterns:
TimeLock Features:
├── 📋 Project Management
├── ✅ Task Management (with subtasks)
├── 📅 Calendar View
├── ⏰ Time Planning
├── 🔍 Search & Filtering
├── 🏷️ Tags & Categories
├── 🔄 Recurring Tasks
└── 📱 Responsive Design
Technology Stack
- Angular 20 - Latest version with modern features
 - TypeScript - Strong typing and better developer experience
 - Signals - Reactive state management
 - IndexedDB - Client-side data persistence
 - CSS3 - Modern styling with CSS Grid and Flexbox
 - Playwright - End-to-end testing
 
Architecture Patterns
1. Standalone Component Architecture
TimeLock uses standalone components throughout, eliminating the need for NgModules:
// Traditional approach (NOT used)
@NgModule({
  declarations: [TodoComponent],
  imports: [CommonModule]
})
// Modern approach (USED in TimeLock)
@Component({
  standalone: true,
  imports: [CommonModule, FormsModule]
})
export class TodoComponent { }
2. Signal-Based State Management
Instead of RxJS Observables for local state, TimeLock uses Angular Signals:
// Service layer
@Injectable({ providedIn: 'root' })
export class TodoService {
  private todosSignal = signal<Todo[]>([]);
  
  // Read-only access for components
  todos = this.todosSignal.asReadonly();
  
  // Computed derived state
  activeTodos = computed(() => 
    this.todosSignal().filter(todo => !todo.completed)
  );
}
3. Dependency Injection Pattern
Services are injected using the modern inject() function:
export class TodoComponent {
  private todoService = inject(TodoService);
  private projectService = inject(ProjectService);
  private router = inject(Router);
  
  // Clean, functional approach
}
4. Lazy Loading Strategy
Routes are lazy-loaded for better performance:
export const routes: Routes = [
  {
    path: 'projects',
    loadComponent: () => import('./pages/projects/projects.component')
      .then(m => m.ProjectsComponent)
  }
];
State Management with Signals
TimeLock demonstrates a comprehensive signal-based state management pattern:
Service-Level State
@Injectable({ providedIn: 'root' })
export class TodoService {
  // Private writable signals
  private todosSignal = signal<Todo[]>([]);
  private filterSignal = signal<FilterType>('all');
  private searchTermSignal = signal<string>('');
  
  // Public read-only signals
  todos = this.todosSignal.asReadonly();
  filter = this.filterSignal.asReadonly();
  searchTerm = this.searchTermSignal.asReadonly();
  
  // Computed signals for derived state
  filteredTodos = computed(() => {
    const todos = this.todosSignal();
    const filter = this.filterSignal();
    const searchTerm = this.searchTermSignal();
    
    return todos
      .filter(todo => this.matchesFilter(todo, filter))
      .filter(todo => this.matchesSearch(todo, searchTerm));
  });
  
  activeTodos = computed(() => 
    this.todosSignal().filter(todo => !todo.completed && !todo.archived)
  );
  
  completedTodos = computed(() => 
    this.todosSignal().filter(todo => todo.completed)
  );
}
Component-Level State
export class TodoItemComponent {
  @Input() todo!: Todo;
  
  // Local component state
  isEditing = signal(false);
  isExpanded = signal(false);
  
  // Computed local state
  hasSubtasks = computed(() => this.todo.subtasks.length > 0);
  completionPercentage = computed(() => {
    if (!this.hasSubtasks()) return 0;
    const completed = this.todo.subtasks.filter(t => t.completed).length;
    return (completed / this.todo.subtasks.length) * 100;
  });
}
Data Flow
TimeLock follows a unidirectional data flow pattern:
┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Components    │───▶│    Services      │───▶│   IndexedDB     │
│                 │    │                  │    │                 │
│ - Display data  │    │ - Business logic │    │ - Data storage  │
│ - Handle events │    │ - State management│    │ - Persistence   │
│ - User input    │    │ - API calls      │    │                 │
└─────────────────┘    └──────────────────┘    └─────────────────┘
         ▲                        │
         │                        ▼
         └────── Signals ─────────┘
Example Data Flow: Adding a Todo
- User Action: User clicks "Add Todo" button
 - Component: TodoFormComponent emits event
 - Parent Component: Calls TodoService.addTodo()
 - Service: Updates signal and persists to IndexedDB
 - Automatic Update: All components using the signal automatically re-render
 
// 1. User clicks button in template
<button (click)="addTodo()">Add Todo</button>
// 2. Component method
addTodo() {
  this.todoService.addTodo(this.newTodo);
}
// 3. Service updates state
async addTodo(todo: Omit<Todo, 'id'>) {
  const newTodo = { ...todo, id: this.generateId() };
  this.todosSignal.update(todos => [...todos, newTodo]);
  await this.indexedDBService.saveTodos(this.todosSignal());
}
// 4. All components automatically update via signals
Component Hierarchy
TimeLock's component structure follows a clear hierarchy:
App (Root Component)
├── Sidebar Navigation
├── Router Outlet
    ├── ProjectsComponent
    │   ├── ProjectCardComponent
    │   └── ProjectFormComponent
    ├── ProjectDetailComponent
    │   ├── TodoFiltersComponent
    │   ├── TodoFormComponent
    │   └── TodoItemComponent
    │       ├── TaskEditorComponent
    │       └── SubtaskComponent
    ├── CalendarComponent
    ├── TimetableComponent
    ├── UpcomingComponent
    └── SearchComponent
Smart vs Dumb Components
Smart Components (Container Components):
- Inject services
 - Manage state
 - Handle business logic
 - Examples: 
ProjectsComponent,ProjectDetailComponent 
export class ProjectsComponent {
  private projectService = inject(ProjectService);
  private todoService = inject(TodoService);
  
  projects = this.projectService.activeProjects;
  
  async createProject(projectData: CreateProjectData) {
    await this.projectService.addProject(projectData);
  }
}
Dumb Components (Presentation Components):
- Receive data via 
@Input() - Emit events via 
@Output() - No service dependencies
 - Examples: 
TodoItemComponent,ProjectCardComponent 
export class TodoItemComponent {
  @Input() todo!: Todo;
  @Output() todoUpdated = new EventEmitter<Todo>();
  @Output() todoDeleted = new EventEmitter<string>();
  
  toggleComplete() {
    this.todoUpdated.emit({
      ...this.todo,
      completed: !this.todo.completed
    });
  }
}
Service Layer
TimeLock's service layer is organized by domain:
Core Services
TodoService - Task management
@Injectable({ providedIn: 'root' })
export class TodoService {
  // CRUD operations
  async addTodo(todo: Omit<Todo, 'id'>): Promise<string>
  async updateTodo(id: string, updates: Partial<Todo>): Promise<void>
  async deleteTodo(id: string): Promise<void>
  
  // Business logic
  async toggleComplete(id: string): Promise<void>
  async addSubtask(parentId: string, subtask: Omit<Todo, 'id'>): Promise<void>
  async autoPlanTasks(tasks: Todo[], timeSlots: TimeSlot[]): Promise<void>
}
ProjectService - Project management
@Injectable({ providedIn: 'root' })
export class ProjectService {
  async addProject(project: Omit<Project, 'id'>): Promise<string>
  async updateProject(id: string, updates: Partial<Project>): Promise<void>
  async deleteProject(id: string): Promise<void>
  setCurrentProject(projectId: string | null): void
}
IndexedDBService - Data persistence
@Injectable({ providedIn: 'root' })
export class IndexedDBService {
  async saveTodos(todos: Todo[]): Promise<void>
  async loadTodos(): Promise<Todo[]>
  async saveProjects(projects: Project[]): Promise<void>
  async loadProjects(): Promise<Project[]>
}
Utility Services
TagsService - Tag management RecurringTasksService - Recurring task logic ConfirmationService - Dialog management
Routing Strategy
TimeLock uses a feature-based routing structure:
export const routes: Routes = [
  { path: '', redirectTo: '/projects', pathMatch: 'full' },
  
  // Main pages
  { path: 'projects', loadComponent: () => import('./pages/projects/projects.component') },
  { path: 'project/:id', loadComponent: () => import('./pages/project-detail/project-detail.component') },
  
  // Views
  { path: 'calendar', loadComponent: () => import('./pages/calendar/calendar.component') },
  { path: 'timetable', loadComponent: () => import('./pages/timetable/timetable.component') },
  { path: 'upcoming', loadComponent: () => import('./pages/upcoming/upcoming.component') },
  { path: 'search', loadComponent: () => import('./pages/search/search.component') },
  
  // Fallback
  { path: '**', redirectTo: '/projects' }
];
Route Guards and Resolvers
While not currently implemented, TimeLock's architecture supports:
// Example guard
export const projectExistsGuard: CanActivateFn = (route) => {
  const projectService = inject(ProjectService);
  const projectId = route.paramMap.get('id');
  return projectService.getProjectById(projectId!) !== undefined;
};
// Example resolver
export const projectResolver: ResolveFn<Project> = (route) => {
  const projectService = inject(ProjectService);
  const projectId = route.paramMap.get('id')!;
  return projectService.getProjectById(projectId)!;
};
Data Persistence
TimeLock uses IndexedDB for client-side data persistence:
Why IndexedDB?
- Offline capability - Works without internet
 - Large storage - Much more than localStorage
 - Structured data - Supports complex objects
 - Asynchronous - Non-blocking operations
 
Data Schema
interface DBSchema {
  todos: {
    key: string;
    value: Todo;
    indexes: { 'by-project': string; 'by-due-date': Date; };
  };
  projects: {
    key: string;
    value: Project;
  };
  tags: {
    key: string;
    value: Tag;
  };
}
Service Integration
@Injectable({ providedIn: 'root' })
export class IndexedDBService {
  private dbName = 'TimeLockDB';
  private version = 1;
  
  async initDB(): Promise<IDBDatabase> {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve(request.result);
      
      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        this.createObjectStores(db);
      };
    });
  }
}
Performance Considerations
1. Lazy Loading
- Routes are loaded on-demand
 - Reduces initial bundle size
 - Faster application startup
 
2. OnPush Change Detection
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  // ...
})
export class OptimizedComponent { }
3. Signal-Based Updates
- Automatic dependency tracking
 - Minimal re-renders
 - Better performance than traditional observables
 
4. Virtual Scrolling (Future Enhancement)
// For large lists
<cdk-virtual-scroll-viewport itemSize="50">
  <div *cdkVirtualFor="let todo of todos">{{todo.title}}</div>
</cdk-virtual-scroll-viewport>
Next Steps
Continue learning about TimeLock:
- Getting Started - Set up your development environment
 - Code Structure - Detailed code organization
 - Development Guide - How to extend the application