Skip to main content

Code Structure

This guide provides a detailed walkthrough of TimeLock's codebase, explaining how files are organized and how different parts work together.

Table of Contents​

  1. Directory Structure
  2. Application Bootstrap
  3. Component Architecture
  4. Service Layer
  5. Models and Types
  6. Routing System
  7. Styling Architecture
  8. Testing Structure

Directory Structure​

src/
├── app/
│ ├── components/ # Reusable UI components
│ │ ├── auto-plan-dialog/
│ │ ├── confirmation-dialog/
│ │ ├── import-dialog/
│ │ ├── parent-selection-dialog/
│ │ ├── task-editor/
│ │ ├── todo-filters/
│ │ ├── todo-form/
│ │ └── todo-item/
│ ├── pages/ # Route-level components
│ │ ├── calendar/
│ │ ├── project-detail/
│ │ ├── projects/
│ │ ├── search/
│ │ ├── timetable/
│ │ └── upcoming/
│ ├── services/ # Business logic and data
│ │ ├── confirmation.service.ts
│ │ ├── indexeddb.service.ts
│ │ ├── project.service.ts
│ │ ├── recurring-tasks.service.ts
│ │ ├── tags.service.ts
│ │ └── todo.service.ts
│ ├── models/ # TypeScript interfaces
│ │ ├── project.model.ts
│ │ └── todo.model.ts
│ ├── task-item/ # Legacy components (being refactored)
│ ├── task-list/
│ ├── app.config.ts # Application configuration
│ ├── app.routes.ts # Route definitions
│ ├── app.ts # Root component
│ └── app.css # Root component styles
├── assets/ # Static assets
│ ├── calendar_logo.svg
│ ├── logo.svg
│ ├── timetable_logo.svg
│ └── upcoming_logo.svg
├── index.html # Main HTML file
├── main.ts # Application entry point
└── styles.css # Global styles

Application Bootstrap​

Entry Point: main.ts​

import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';

bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));

Key Points:

  • Uses bootstrapApplication() for standalone components
  • No AppModule needed (modern Angular approach)
  • Configuration is externalized to app.config.ts

Application Configuration: app.config.ts​

import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes)
]
};

Configuration Features:

  • Global error handling - Catches unhandled errors
  • Zone.js optimization - Event coalescing for performance
  • Router configuration - Enables routing functionality

Root Component: app.ts​

@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, RouterModule],
template: `
<div class="app-container">
<!-- Sidebar Toggle Button -->
<button class="sidebar-toggle" (click)="toggleSidebar()">
<!-- Hamburger menu -->
</button>

<!-- Sidebar Navigation -->
<aside class="sidebar" [class.open]="sidebarOpen()">
<div class="sidebar-header">
<h1 class="app-title">TimeLock</h1>
</div>

<nav class="sidebar-nav">
<!-- Navigation links -->
</nav>

<!-- Projects section -->
<div class="projects-section">
<!-- Project list -->
</div>
</aside>

<!-- Main Content -->
<main class="main-content">
<router-outlet></router-outlet>
</main>
</div>
`
})
export class App {
private projectService = inject(ProjectService);

sidebarOpen = signal(false);
projects = this.projectService.activeProjects;
currentProject = this.projectService.currentProject;
}

Root Component Features:

  • Layout structure - Sidebar + main content
  • Navigation management - Sidebar toggle and routing
  • Project integration - Displays project list
  • Responsive design - Mobile-friendly sidebar

Component Architecture​

Component Categories​

1. Page Components (src/app/pages/)

  • Route-level components
  • Container components that manage state
  • Coordinate multiple child components

2. Feature Components (src/app/components/)

  • Reusable UI components
  • Presentation components with inputs/outputs
  • Focused on specific functionality

3. Dialog Components

  • Modal dialogs for user interactions
  • Confirmation dialogs, forms, selection dialogs

Example: TodoItem Component​

// src/app/components/todo-item/todo-item.component.ts
@Component({
selector: 'app-todo-item',
standalone: true,
imports: [CommonModule, FormsModule, TaskEditorComponent],
templateUrl: './todo-item.component.html',
styleUrls: ['./todo-item.component.css']
})
export class TodoItemComponent {
@Input() todo!: Todo;
@Input() depth: number = 0;
@Input() showProject: boolean = false;

@Output() todoUpdated = new EventEmitter<Todo>();
@Output() todoDeleted = new EventEmitter<string>();
@Output() subtaskAdded = new EventEmitter<{parentId: string, subtask: Omit<Todo, 'id'>}>();

// Local state
isEditing = signal(false);
isExpanded = signal(false);
showSubtasks = signal(true);

// Computed properties
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;
});

// Methods
toggleComplete() {
this.todoUpdated.emit({
...this.todo,
completed: !this.todo.completed,
updatedAt: new Date()
});
}

startEditing() {
this.isEditing.set(true);
}

saveEdit(updatedTodo: Todo) {
this.isEditing.set(false);
this.todoUpdated.emit(updatedTodo);
}
}

Component Structure:

  • Inputs - Data from parent components
  • Outputs - Events to parent components
  • Local state - Component-specific state with signals
  • Computed properties - Derived state
  • Methods - Event handlers and business logic

Component Communication Patterns​

1. Parent-Child Communication

// Parent template
<app-todo-item
[todo]="todo"
[depth]="0"
(todoUpdated)="handleTodoUpdate($event)"
(todoDeleted)="handleTodoDelete($event)">
</app-todo-item>

// Parent component
handleTodoUpdate(updatedTodo: Todo) {
this.todoService.updateTodo(updatedTodo.id, updatedTodo);
}

2. Service-Based Communication

// Components communicate through shared services
export class ComponentA {
private todoService = inject(TodoService);

addTodo(todo: Todo) {
this.todoService.addTodo(todo); // Updates signal
}
}

export class ComponentB {
private todoService = inject(TodoService);

todos = this.todoService.todos; // Reactive to changes
}

Service Layer​

Service Organization​

Core Services:

  • TodoService - Task management
  • ProjectService - Project management
  • IndexedDBService - Data persistence

Utility Services:

  • TagsService - Tag management
  • RecurringTasksService - Recurring task logic
  • ConfirmationService - Dialog management

Example: TodoService Structure​

@Injectable({ providedIn: 'root' })
export class TodoService {
private indexedDBService = inject(IndexedDBService);

// Private signals (writable)
private todosSignal = signal<Todo[]>([]);
private filterSignal = signal<FilterType>('all');
private searchTermSignal = signal<string>('');
private sortTypeSignal = signal<SortType>('created');

// Public signals (readonly)
todos = this.todosSignal.asReadonly();
filter = this.filterSignal.asReadonly();
searchTerm = this.searchTermSignal.asReadonly();
sortType = this.sortTypeSignal.asReadonly();

// Computed signals
filteredTodos = computed(() => {
let todos = this.todosSignal();
const filter = this.filterSignal();
const searchTerm = this.searchTermSignal().toLowerCase();

// Apply filters
todos = this.applyFilter(todos, filter);
todos = this.applySearch(todos, searchTerm);
todos = this.applySorting(todos, this.sortTypeSignal());

return todos;
});

activeTodos = computed(() =>
this.todosSignal().filter(todo => !todo.completed && !todo.archived)
);

completedTodos = computed(() =>
this.todosSignal().filter(todo => todo.completed)
);

// CRUD operations
async addTodo(todoData: Omit<Todo, 'id' | 'createdAt' | 'updatedAt'>): Promise<string> {
const newTodo: Todo = {
...todoData,
id: this.generateId(),
createdAt: new Date(),
updatedAt: new Date(),
subtasks: [],
isExpanded: false,
order: this.getNextOrder()
};

this.todosSignal.update(todos => [...todos, newTodo]);
await this.saveTodos();
return newTodo.id;
}

async updateTodo(id: string, updates: Partial<Todo>): Promise<void> {
this.todosSignal.update(todos =>
todos.map(todo =>
todo.id === id
? { ...todo, ...updates, updatedAt: new Date() }
: todo
)
);
await this.saveTodos();
}

async deleteTodo(id: string): Promise<void> {
this.todosSignal.update(todos =>
this.removeTodoRecursively(todos, id)
);
await this.saveTodos();
}

// Business logic methods
async toggleComplete(id: string): Promise<void> {
const todo = this.findTodoById(id);
if (todo) {
await this.updateTodo(id, { completed: !todo.completed });
}
}

async addSubtask(parentId: string, subtaskData: Omit<Todo, 'id'>): Promise<string> {
// Implementation for adding subtasks
}

// Utility methods
private generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}

private async saveTodos(): Promise<void> {
await this.indexedDBService.saveTodos(this.todosSignal());
}
}

Service Patterns:

  • Signal-based state - Reactive state management
  • Computed derived state - Automatic updates
  • Async operations - Promise-based API
  • Error handling - Try-catch blocks
  • Data persistence - Automatic saving

IndexedDB Service​

@Injectable({ providedIn: 'root' })
export class IndexedDBService {
private dbName = 'TimeLockDB';
private version = 1;
private db: IDBDatabase | null = null;

async initDB(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);

request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};

request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
this.createObjectStores(db);
};
});
}

private createObjectStores(db: IDBDatabase): void {
// Create todos store
if (!db.objectStoreNames.contains('todos')) {
const todosStore = db.createObjectStore('todos', { keyPath: 'id' });
todosStore.createIndex('projectId', 'projectId', { unique: false });
todosStore.createIndex('dueDate', 'dueDate', { unique: false });
}

// Create projects store
if (!db.objectStoreNames.contains('projects')) {
db.createObjectStore('projects', { keyPath: 'id' });
}
}

async saveTodos(todos: Todo[]): Promise<void> {
if (!this.db) await this.initDB();

const transaction = this.db!.transaction(['todos'], 'readwrite');
const store = transaction.objectStore('todos');

// Clear existing data
await store.clear();

// Add all todos
for (const todo of todos) {
await store.add(todo);
}
}

async loadTodos(): Promise<Todo[]> {
if (!this.db) await this.initDB();

const transaction = this.db!.transaction(['todos'], 'readonly');
const store = transaction.objectStore('todos');
const request = store.getAll();

return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result || []);
request.onerror = () => reject(request.error);
});
}
}

Models and Types​

Todo Model​

// src/app/models/todo.model.ts
export interface Todo {
id: string;
title: string;
description?: string;
completed: boolean;
archived: boolean;
priority: 'low' | 'medium' | 'high';
dueDate?: Date;
startDateTime?: Date;
endDateTime?: Date;
duration?: number;
createdAt: Date;
updatedAt: Date;
category?: string;
parentId?: string;
projectId?: string;
subtasks: Todo[];
isExpanded: boolean;
order: number;
isAutoPlanned?: boolean;
tags?: string[];
isRecurring?: boolean;
recurrencePattern?: RecurrencePattern;
originalTaskId?: string;
nextDueDate?: Date;
}

export interface RecurrencePattern {
type: 'daily' | 'weekly' | 'monthly';
interval: number;
endDate?: Date;
maxOccurrences?: number;
currentOccurrences?: number;
}

export interface Tag {
name: string;
color: string;
count?: number;
}

export type FilterType = 'all' | 'active' | 'completed' | 'archived';
export type SortType = 'created' | 'priority' | 'dueDate' | 'alphabetical';

Project Model​

// src/app/models/project.model.ts
export interface Project {
id: string;
name: string;
description?: string;
color: string;
icon?: string;
createdAt: Date;
updatedAt: Date;
isArchived: boolean;
order: number;
}

export interface ProjectStats {
totalTasks: number;
completedTasks: number;
activeTasks: number;
overdueTasks: number;
highPriorityTasks: number;
}

Model Design Principles:

  • Immutable updates - Always create new objects
  • Optional properties - Use ? for optional fields
  • Union types - Restrict values with type unions
  • Date objects - Use Date type for temporal data
  • Nested structures - Support complex hierarchies

Routing System​

Route Configuration​

// src/app/app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
{
path: '',
redirectTo: '/projects',
pathMatch: 'full'
},
{
path: 'projects',
loadComponent: () => import('./pages/projects/projects.component')
.then(m => m.ProjectsComponent)
},
{
path: 'project/:id',
loadComponent: () => import('./pages/project-detail/project-detail.component')
.then(m => m.ProjectDetailComponent)
},
{
path: 'calendar',
loadComponent: () => import('./pages/calendar/calendar.component')
.then(m => m.CalendarComponent)
},
{
path: 'timetable',
loadComponent: () => import('./pages/timetable/timetable.component')
.then(m => m.TimetableComponent)
},
{
path: 'upcoming',
loadComponent: () => import('./pages/upcoming/upcoming.component')
.then(m => m.UpcomingComponent)
},
{
path: 'search',
loadComponent: () => import('./pages/search/search.component')
.then(m => m.SearchComponent)
},
{
path: '**',
redirectTo: '/projects'
}
];

Routing Features:

  • Lazy loading - Components loaded on demand
  • Route parameters - Dynamic routes with parameters
  • Redirects - Default and fallback routes
  • Wildcard routes - Handle unknown routes

Route Parameters​

// In ProjectDetailComponent
export class ProjectDetailComponent {
private route = inject(ActivatedRoute);
private projectService = inject(ProjectService);

projectId = signal<string | null>(null);
project = computed(() => {
const id = this.projectId();
return id ? this.projectService.getProjectById(id) : null;
});

ngOnInit() {
// Get route parameter
const id = this.route.snapshot.paramMap.get('id');
this.projectId.set(id);

// Set current project
if (id) {
this.projectService.setCurrentProject(id);
}
}
}

Styling Architecture​

Global Styles​

/* src/styles.css */
:root {
/* Color variables */
--primary-color: #3b82f6;
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;

/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;

/* Typography */
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
}

/* Reset and base styles */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #374151;
background-color: #f9fafb;
}

Component Styles​

/* Component-specific styles */
.todo-item {
display: flex;
align-items: center;
padding: var(--spacing-md);
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
background: white;
margin-bottom: var(--spacing-sm);
transition: all 0.2s ease;
}

.todo-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.todo-item.completed {
opacity: 0.6;
text-decoration: line-through;
}

Styling Patterns:

  • CSS Custom Properties - Consistent design tokens
  • BEM methodology - Clear class naming
  • Component scoping - Styles scoped to components
  • Responsive design - Mobile-first approach

Testing Structure​

Unit Tests​

// Component test example
describe('TodoItemComponent', () => {
let component: TodoItemComponent;
let fixture: ComponentFixture<TodoItemComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TodoItemComponent]
}).compileComponents();

fixture = TestBed.createComponent(TodoItemComponent);
component = fixture.componentInstance;
component.todo = mockTodo;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should emit todoUpdated when toggling complete', () => {
spyOn(component.todoUpdated, 'emit');
component.toggleComplete();
expect(component.todoUpdated.emit).toHaveBeenCalled();
});
});

E2E Tests​

// e2e/app.spec.ts
import { test, expect } from '@playwright/test';

test('should create a new todo', async ({ page }) => {
await page.goto('/');

// Navigate to projects
await page.click('[data-testid="projects-link"]');

// Add new todo
await page.fill('[data-testid="todo-input"]', 'New test todo');
await page.click('[data-testid="add-todo-button"]');

// Verify todo appears
await expect(page.locator('[data-testid="todo-item"]')).toContainText('New test todo');
});

Next Steps​

Now that you understand the code structure:

  1. Start developing - Development Guide
  2. Learn the features - Features Overview
  3. Understand architecture - Project Architecture

The codebase follows modern Angular patterns and best practices, making it an excellent learning resource for Angular development.