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
- Directory Structure
- Application Bootstrap
- Component Architecture
- Service Layer
- Models and Types
- Routing System
- Styling Architecture
- 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 managementProjectService
- Project managementIndexedDBService
- Data persistence
Utility Services:
TagsService
- Tag managementRecurringTasksService
- Recurring task logicConfirmationService
- 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:
- Start developing - Development Guide
- Learn the features - Features Overview
- Understand architecture - Project Architecture
The codebase follows modern Angular patterns and best practices, making it an excellent learning resource for Angular development.