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.