├── .editorconfig
├── .babelrc
├── .eslintignore
├── first-ng-app
├── src
│ ├── app
│ │ ├── components
│ │ │ ├── counter
│ │ │ │ ├── counter.component.scss
│ │ │ │ ├── counter.component.html
│ │ │ │ ├── counter.component.ts
│ │ │ │ └── counter.component.spec.ts
│ │ │ ├── greeting
│ │ │ │ ├── greeting.component.scss
│ │ │ │ ├── greeting.component.html
│ │ │ │ ├── greeting.component.ts
│ │ │ │ └── greeting.component.spec.ts
│ │ │ ├── todo-item
│ │ │ │ ├── todo-item.component.scss
│ │ │ │ ├── todo-item.component.html
│ │ │ │ ├── todo-item.component.spec.ts
│ │ │ │ └── todo-item.component.ts
│ │ │ └── header
│ │ │ │ ├── header.component.html
│ │ │ │ ├── header.component.ts
│ │ │ │ ├── header.component.scss
│ │ │ │ └── header.component.spec.ts
│ │ ├── home
│ │ │ ├── home.component.scss
│ │ │ ├── home.component.html
│ │ │ ├── home.component.ts
│ │ │ └── home.component.spec.ts
│ │ ├── model
│ │ │ └── todo.type.ts
│ │ ├── todos
│ │ │ ├── todos.component.scss
│ │ │ ├── todos.component.html
│ │ │ ├── todos.component.spec.ts
│ │ │ └── todos.component.ts
│ │ ├── pipes
│ │ │ ├── filter-todos.pipe.spec.ts
│ │ │ └── filter-todos.pipe.ts
│ │ ├── directives
│ │ │ ├── highlight-completed-todo.directive.spec.ts
│ │ │ └── highlight-completed-todo.directive.ts
│ │ ├── services
│ │ │ ├── todos.service.spec.ts
│ │ │ └── todos.service.ts
│ │ ├── app.routes.ts
│ │ ├── app.config.ts
│ │ ├── app.component.ts
│ │ └── app.component.spec.ts
│ ├── styles.scss
│ ├── main.ts
│ └── index.html
├── public
│ └── favicon.ico
├── .vscode
│ ├── extensions.json
│ ├── launch.json
│ └── tasks.json
├── .editorconfig
├── tsconfig.app.json
├── tsconfig.spec.json
├── .gitignore
├── tsconfig.json
├── README.md
├── package.json
└── angular.json
├── postcss.config.js
├── .npmignore
├── assets
└── images
│ ├── banner.png
│ ├── gde-logo.png
│ ├── code with ahsan.png
│ ├── ng-cookbook-2.png
│ ├── mastering-angular-signals.png
│ ├── app-mockup.svg
│ ├── components-1.svg
│ ├── home-with-nested-components.svg
│ ├── todos-route.svg
│ └── todo-item.svg
├── data
└── slides.json
├── .gitignore
├── .prettierrc
├── tailwind.config.js
├── .eslintrc.json
├── content
├── my-slides.md
├── slides.html
├── static-slides.html
└── angular-in-90ish.md
├── js
└── slides.js
├── main.js
├── .github
├── CONTRIBUTING.md
└── workflows
│ └── deploy.yml
├── LICENSE
├── index.html
├── scripts
├── addIdsToSlide.js
└── extractSlideData.js
├── profiles
└── ahsan.md
├── README.md
├── css
└── slides.scss
├── package.json
└── webpack.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | {
2 | printLength: 80
3 | }
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"]
3 | }
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | js/controllers/
2 | js/utils/
3 | plugin
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/counter/counter.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/greeting/greeting.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/todo-item/todo-item.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/home/home.component.scss:
--------------------------------------------------------------------------------
1 | input {
2 | margin-top: 10px;
3 | }
4 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require('autoprefixer')],
3 | };
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /test
2 | /examples
3 | .github
4 | .gulpfile
5 | .sass-cache
6 | gulpfile.js
7 | CONTRIBUTING.md
--------------------------------------------------------------------------------
/assets/images/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AhsanAyaz/angular-in-90ish/HEAD/assets/images/banner.png
--------------------------------------------------------------------------------
/assets/images/gde-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AhsanAyaz/angular-in-90ish/HEAD/assets/images/gde-logo.png
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/greeting/greeting.component.html:
--------------------------------------------------------------------------------
1 |
Greetings!
2 | {{ message() }}
3 |
--------------------------------------------------------------------------------
/assets/images/code with ahsan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AhsanAyaz/angular-in-90ish/HEAD/assets/images/code with ahsan.png
--------------------------------------------------------------------------------
/assets/images/ng-cookbook-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AhsanAyaz/angular-in-90ish/HEAD/assets/images/ng-cookbook-2.png
--------------------------------------------------------------------------------
/data/slides.json:
--------------------------------------------------------------------------------
1 | [{"link":"angular-in-90ish.md","title":"Angular in 90-ish minutes"},{"link":"my-slides.md","title":"My Slides"}]
--------------------------------------------------------------------------------
/first-ng-app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AhsanAyaz/angular-in-90ish/HEAD/first-ng-app/public/favicon.ico
--------------------------------------------------------------------------------
/assets/images/mastering-angular-signals.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AhsanAyaz/angular-in-90ish/HEAD/assets/images/mastering-angular-signals.png
--------------------------------------------------------------------------------
/first-ng-app/src/app/model/todo.type.ts:
--------------------------------------------------------------------------------
1 | export type Todo = {
2 | userId: number;
3 | completed: boolean;
4 | title: string;
5 | id: number;
6 | };
7 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/todos/todos.component.scss:
--------------------------------------------------------------------------------
1 | .todos {
2 | &__item {
3 | display: flex;
4 | align-items: center;
5 | gap: 8px;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | *.iml
3 | *.iws
4 | *.eml
5 | out/
6 | .DS_Store
7 | .svn
8 | log/*.log
9 | tmp/**
10 | node_modules/
11 | .sass-cache
12 | build
13 | dist
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "singleQuote": true,
4 | "useTabs": false,
5 | "tabWidth": 2,
6 | "semi": true,
7 | "bracketSpacing": true
8 | }
9 |
--------------------------------------------------------------------------------
/first-ng-app/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
3 | "recommendations": ["angular.ng-template"]
4 | }
5 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ['./content/**/*.{html,js,css}', './css/**/*.scss'],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | };
9 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/home/home.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/header/header.component.html:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/first-ng-app/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
3 | html,
4 | body {
5 | margin: 0;
6 | padding: 0;
7 | }
8 |
9 | * {
10 | outline: none;
11 | }
12 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/counter/counter.component.html:
--------------------------------------------------------------------------------
1 | Counter value: {{ counterValue() }}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/pipes/filter-todos.pipe.spec.ts:
--------------------------------------------------------------------------------
1 | import { FilterTodosPipe } from './filter-todos.pipe';
2 |
3 | describe('FilterTodosPipe', () => {
4 | it('create an instance', () => {
5 | const pipe = new FilterTodosPipe();
6 | expect(pipe).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/first-ng-app/src/main.ts:
--------------------------------------------------------------------------------
1 | import { bootstrapApplication } from '@angular/platform-browser';
2 | import { appConfig } from './app/app.config';
3 | import { AppComponent } from './app/app.component';
4 |
5 | bootstrapApplication(AppComponent, appConfig)
6 | .catch((err) => console.error(err));
7 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/directives/highlight-completed-todo.directive.spec.ts:
--------------------------------------------------------------------------------
1 | import { HighlightCompletedTodoDirective } from './highlight-completed-todo.directive';
2 |
3 | describe('HighlightCompletedTodoDirective', () => {
4 | it('should create an instance', () => {
5 | const directive = new HighlightCompletedTodoDirective();
6 | expect(directive).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/greeting/greeting.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, input } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-greeting',
5 | standalone: true,
6 | imports: [],
7 | templateUrl: './greeting.component.html',
8 | styleUrl: './greeting.component.scss',
9 | })
10 | export class GreetingComponent {
11 | message = input('Hello hello!');
12 | }
13 |
--------------------------------------------------------------------------------
/first-ng-app/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FirstNgApp
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/first-ng-app/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 | ij_typescript_use_double_quotes = false
14 |
15 | [*.md]
16 | max_line_length = off
17 | trim_trailing_whitespace = false
18 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/todo-item/todo-item.component.html:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/header/header.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { RouterLink } from '@angular/router';
3 |
4 | @Component({
5 | selector: 'app-header',
6 | standalone: true,
7 | imports: [RouterLink],
8 | templateUrl: './header.component.html',
9 | styleUrl: './header.component.scss',
10 | })
11 | export class HeaderComponent {
12 | title = 'My first Angular app';
13 | }
14 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/services/todos.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { TodosService } from './todos.service';
4 |
5 | describe('TodosService', () => {
6 | let service: TodosService;
7 |
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({});
10 | service = TestBed.inject(TodosService);
11 | });
12 |
13 | it('should be created', () => {
14 | expect(service).toBeTruthy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/services/todos.service.ts:
--------------------------------------------------------------------------------
1 | import { inject, Injectable } from '@angular/core';
2 | import { Todo } from '../model/todo.type';
3 | import { HttpClient } from '@angular/common/http';
4 |
5 | @Injectable({
6 | providedIn: 'root',
7 | })
8 | export class TodosService {
9 | http = inject(HttpClient);
10 | getTodosFromApi() {
11 | const url = `https://jsonplaceholder.typicode.com/todos`;
12 | return this.http.get>(url);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/app.routes.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@angular/router';
2 |
3 | export const routes: Routes = [
4 | {
5 | path: '',
6 | pathMatch: 'full',
7 | loadComponent: () => {
8 | return import('./home/home.component').then((m) => m.HomeComponent);
9 | },
10 | },
11 | {
12 | path: 'todos',
13 | loadComponent: () => {
14 | return import('./todos/todos.component').then((m) => m.TodosComponent);
15 | },
16 | },
17 | ];
18 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/app.config.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
2 | import { provideRouter } from '@angular/router';
3 |
4 | import { routes } from './app.routes';
5 | import { provideHttpClient } from '@angular/common/http';
6 |
7 | export const appConfig: ApplicationConfig = {
8 | providers: [
9 | provideHttpClient(),
10 | provideZoneChangeDetection({ eventCoalescing: true }),
11 | provideRouter(routes),
12 | ],
13 | };
14 |
--------------------------------------------------------------------------------
/first-ng-app/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "./tsconfig.json",
5 | "compilerOptions": {
6 | "outDir": "./out-tsc/app",
7 | "types": []
8 | },
9 | "files": [
10 | "src/main.ts"
11 | ],
12 | "include": [
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/first-ng-app/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "./tsconfig.json",
5 | "compilerOptions": {
6 | "outDir": "./out-tsc/spec",
7 | "types": [
8 | "jasmine"
9 | ]
10 | },
11 | "include": [
12 | "src/**/*.spec.ts",
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/todos/todos.component.html:
--------------------------------------------------------------------------------
1 | Todos List
2 |
3 | @if (!todoItems().length) {
4 | Loading...
5 | }
6 |
7 |
15 |
16 |
17 | @for (todo of todoItems() | filterTodos : searchTerm(); track todo.id) {
18 |
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "ecmaVersion": "latest", // or a version high enough to support ES6+
4 | "sourceType": "module", // Allows use of imports
5 | "ecmaFeatures": {
6 | "jsx": true // if you are using React
7 | }
8 | },
9 | "env": {
10 | "browser": true,
11 | "es2021": true
12 | },
13 | "extends": [
14 | "eslint:recommended",
15 | // Add react presets if you're using React
16 | "plugin:react/recommended"
17 | ],
18 | "rules": {
19 | // Your custom rules
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/pipes/filter-todos.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 | import { Todo } from '../model/todo.type';
3 |
4 | @Pipe({
5 | name: 'filterTodos',
6 | standalone: true,
7 | })
8 | export class FilterTodosPipe implements PipeTransform {
9 | transform(todos: Todo[], searchTerm: string): Todo[] {
10 | if (!searchTerm) {
11 | return todos;
12 | }
13 | const text = searchTerm.toLowerCase();
14 | return todos.filter((todo) => {
15 | return todo.title.toLowerCase().includes(text);
16 | });
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/first-ng-app/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
3 | "version": "0.2.0",
4 | "configurations": [
5 | {
6 | "name": "ng serve",
7 | "type": "chrome",
8 | "request": "launch",
9 | "preLaunchTask": "npm: start",
10 | "url": "http://localhost:4200/"
11 | },
12 | {
13 | "name": "ng test",
14 | "type": "chrome",
15 | "request": "launch",
16 | "preLaunchTask": "npm: test",
17 | "url": "http://localhost:9876/debug.html"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/counter/counter.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, signal } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-counter',
5 | standalone: true,
6 | imports: [],
7 | templateUrl: './counter.component.html',
8 | styleUrl: './counter.component.scss',
9 | })
10 | export class CounterComponent {
11 | counterValue = signal(0);
12 | increment() {
13 | this.counterValue.update((val) => val + 1);
14 | }
15 |
16 | decrement() {
17 | this.counterValue.update((val) => val - 1);
18 | }
19 |
20 | reset() {
21 | this.counterValue.set(0);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { RouterOutlet } from '@angular/router';
3 | import { HeaderComponent } from './components/header/header.component';
4 |
5 | @Component({
6 | selector: 'app-root',
7 | standalone: true,
8 | imports: [RouterOutlet, HeaderComponent],
9 | template: `
10 |
11 |
12 |
13 |
14 | `,
15 | styles: [
16 | `
17 | main {
18 | padding: 16px;
19 | }
20 | `,
21 | ],
22 | })
23 | export class AppComponent {
24 | title = 'first-ng-app';
25 | }
26 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/home/home.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, signal } from '@angular/core';
2 | import { GreetingComponent } from '../components/greeting/greeting.component';
3 | import { CounterComponent } from '../components/counter/counter.component';
4 |
5 | @Component({
6 | selector: 'app-home',
7 | standalone: true,
8 | imports: [GreetingComponent, CounterComponent],
9 | templateUrl: './home.component.html',
10 | styleUrl: './home.component.scss',
11 | })
12 | export class HomeComponent {
13 | homeMessage = signal('Hello, world!');
14 |
15 | keyUpHandler(event: KeyboardEvent) {
16 | console.log(`user pressed the ${event.key} key`);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/content/my-slides.md:
--------------------------------------------------------------------------------
1 | # My Slides
2 |
3 | Here goes some content in first
4 |
5 | --
6 |
7 | ## Second Slide (vertical)
8 |
9 | more content
10 |
11 | ---
12 |
13 | ## Horizontal slide
14 | ### (horizontal separated)
15 |
16 | Yeah, that can happen
17 |
18 | --
19 |
20 | This is some code
21 |
22 | ```js
23 | const foo = 'bar';
24 | ```
25 |
26 | Code step highlighting
27 |
28 | ```ts [1-3]
29 | const main = async () => {
30 | const resp = await fetch('data/slides.json');
31 | const slides = await resp.json();
32 | const grid = document.getElementById('talksGrid');
33 | slides.forEach((slide) => {
34 | // ...
35 | });
36 | };
37 |
38 | main();
39 |
40 |
41 | ```
--------------------------------------------------------------------------------
/js/slides.js:
--------------------------------------------------------------------------------
1 | import Reveal from 'reveal.js';
2 | import 'reveal.js/dist/reveal.css';
3 | import 'reveal.js/dist/theme/black.css';
4 | import 'reveal.js/plugin/highlight/monokai.css';
5 | import * as RevealHighlight from 'reveal.js/plugin/highlight/highlight.js';
6 | import * as RevealMarkdown from 'reveal.js/plugin/markdown/markdown.js';
7 | import * as RevealNotes from 'reveal.js/plugin/notes/notes.js';
8 | import * as RevealMath from 'reveal.js/plugin/math/math.js';
9 | import '../css/slides.scss';
10 |
11 | Reveal.initialize({
12 | controls: true,
13 | progress: true,
14 | history: true,
15 | center: true,
16 |
17 | plugins: [RevealMarkdown, RevealHighlight, RevealNotes, RevealMath.KaTeX],
18 | });
19 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/home/home.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { HomeComponent } from './home.component';
4 |
5 | describe('HomeComponent', () => {
6 | let component: HomeComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [HomeComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(HomeComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/header/header.component.scss:
--------------------------------------------------------------------------------
1 | header {
2 | display: flex;
3 | padding-inline: 16px;
4 | padding-block: 8px;
5 | background-color: #333;
6 | color: white;
7 | align-items: center;
8 | justify-content: space-between;
9 |
10 | nav {
11 | width: 100%;
12 | display: flex;
13 | justify-content: space-between;
14 | align-items: center;
15 |
16 | > span {
17 | cursor: pointer;
18 | &:hover {
19 | color: #777;
20 | }
21 | }
22 |
23 | ul {
24 | list-style: none;
25 |
26 | li {
27 | cursor: pointer;
28 | &:hover {
29 | color: #777;
30 | }
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/todos/todos.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { TodosComponent } from './todos.component';
4 |
5 | describe('TodosComponent', () => {
6 | let component: TodosComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [TodosComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(TodosComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/header/header.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { HeaderComponent } from './header.component';
4 |
5 | describe('HeaderComponent', () => {
6 | let component: HeaderComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [HeaderComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(HeaderComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/counter/counter.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { CounterComponent } from './counter.component';
4 |
5 | describe('CounterComponent', () => {
6 | let component: CounterComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [CounterComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(CounterComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/greeting/greeting.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { GreetingComponent } from './greeting.component';
4 |
5 | describe('GreetingComponent', () => {
6 | let component: GreetingComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [GreetingComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(GreetingComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/todo-item/todo-item.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { TodoItemComponent } from './todo-item.component';
4 |
5 | describe('TodoItemComponent', () => {
6 | let component: TodoItemComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | imports: [TodoItemComponent]
12 | })
13 | .compileComponents();
14 |
15 | fixture = TestBed.createComponent(TodoItemComponent);
16 | component = fixture.componentInstance;
17 | fixture.detectChanges();
18 | });
19 |
20 | it('should create', () => {
21 | expect(component).toBeTruthy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/components/todo-item/todo-item.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, input, output } from '@angular/core';
2 | import { Todo } from '../../model/todo.type';
3 | import { HighlightCompletedTodoDirective } from '../../directives/highlight-completed-todo.directive';
4 | import { UpperCasePipe } from '@angular/common';
5 |
6 | @Component({
7 | selector: 'app-todo-item',
8 | standalone: true,
9 | imports: [HighlightCompletedTodoDirective, UpperCasePipe],
10 | templateUrl: './todo-item.component.html',
11 | styleUrl: './todo-item.component.scss',
12 | })
13 | export class TodoItemComponent {
14 | todo = input.required();
15 | todoToggled = output();
16 |
17 | todoClicked() {
18 | this.todoToggled.emit(this.todo());
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/first-ng-app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
2 |
3 | # Compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /bazel-out
8 |
9 | # Node
10 | /node_modules
11 | npm-debug.log
12 | yarn-error.log
13 |
14 | # IDEs and editors
15 | .idea/
16 | .project
17 | .classpath
18 | .c9/
19 | *.launch
20 | .settings/
21 | *.sublime-workspace
22 |
23 | # Visual Studio Code
24 | .vscode/*
25 | !.vscode/settings.json
26 | !.vscode/tasks.json
27 | !.vscode/launch.json
28 | !.vscode/extensions.json
29 | .history/*
30 |
31 | # Miscellaneous
32 | /.angular/cache
33 | .sass-cache/
34 | /connect.lock
35 | /coverage
36 | /libpeerconnection.log
37 | testem.log
38 | /typings
39 |
40 | # System files
41 | .DS_Store
42 | Thumbs.db
43 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/directives/highlight-completed-todo.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive, input, effect, inject, ElementRef } from '@angular/core';
2 |
3 | @Directive({
4 | selector: '[appHighlightCompletedTodo]',
5 | standalone: true,
6 | })
7 | export class HighlightCompletedTodoDirective {
8 | isCompleted = input(false);
9 | el = inject(ElementRef);
10 | stylesEffect = effect(() => {
11 | if (this.isCompleted()) {
12 | this.el.nativeElement.style.textDecoration = 'line-through';
13 | this.el.nativeElement.style.backgroundColor = '#d3f9d8';
14 | this.el.nativeElement.style.color = '#6c757d';
15 | } else {
16 | this.el.nativeElement.style.textDecoration = 'none';
17 | this.el.nativeElement.style.backgroundColor = '#fff';
18 | this.el.nativeElement.style.color = '#000';
19 | }
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const main = async () => {
2 | const resp = await fetch('data/slides.json');
3 | const slides = await resp.json();
4 | const grid = document.getElementById('talksGrid');
5 | slides.forEach((slide) => {
6 | const { link, title } = slide;
7 | const sectionEl = document.createElement('section');
8 | sectionEl.className =
9 | 'shadow-md border border-slate-300 rounded-md hover:bg-purple-700 duration-200 hover:text-white cursor-pointer';
10 | const anchorEl = document.createElement('a');
11 | anchorEl.className = 'p-4 w-full h-full block';
12 | anchorEl.href = `slides.html?md=${encodeURIComponent(
13 | link
14 | )}&title=${encodeURIComponent(title)}`;
15 | anchorEl.target = '_blank';
16 | anchorEl.textContent = title;
17 | sectionEl.appendChild(anchorEl);
18 | grid.appendChild(sectionEl);
19 | });
20 | };
21 |
22 | main();
23 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing
2 |
3 | Please keep the [issue tracker](https://github.com/ahsanayaz/angular-in-90ish/issues) limited to **bug reports**.
4 |
5 | ### General Questions and Support
6 |
7 | If you have questions about how to use reveal.js the best place to ask is in the [Discussions](https://github.com/ahsanayaz/angular-in-90ish/discussions). Anything that isn't a bug report should be posted as a dicussion instead.
8 |
9 | ### Bug Reports
10 |
11 | When reporting a bug make sure to include information about which browser and operating system you are on as well as the necessary steps to reproduce the issue. If possible please include a link to a sample presentation where the bug can be tested.
12 |
13 | ### Pull Requests
14 |
15 | - Should be submitted from a feature/topic branch (not your master)
16 | - Should follow the coding style of the file you work in, most importantly:
17 | - Tabs to indent
18 | - Single-quoted strings
19 |
20 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { AppComponent } from './app.component';
3 |
4 | describe('AppComponent', () => {
5 | beforeEach(async () => {
6 | await TestBed.configureTestingModule({
7 | imports: [AppComponent],
8 | }).compileComponents();
9 | });
10 |
11 | it('should create the app', () => {
12 | const fixture = TestBed.createComponent(AppComponent);
13 | const app = fixture.componentInstance;
14 | expect(app).toBeTruthy();
15 | });
16 |
17 | it(`should have the 'first-ng-app' title`, () => {
18 | const fixture = TestBed.createComponent(AppComponent);
19 | const app = fixture.componentInstance;
20 | expect(app.title).toEqual('first-ng-app');
21 | });
22 |
23 | it('should render title', () => {
24 | const fixture = TestBed.createComponent(AppComponent);
25 | fixture.detectChanges();
26 | const compiled = fixture.nativeElement as HTMLElement;
27 | expect(compiled.querySelector('h1')?.textContent).toContain('Hello, first-ng-app');
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/first-ng-app/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
3 | "version": "2.0.0",
4 | "tasks": [
5 | {
6 | "type": "npm",
7 | "script": "start",
8 | "isBackground": true,
9 | "problemMatcher": {
10 | "owner": "typescript",
11 | "pattern": "$tsc",
12 | "background": {
13 | "activeOnStart": true,
14 | "beginsPattern": {
15 | "regexp": "(.*?)"
16 | },
17 | "endsPattern": {
18 | "regexp": "bundle generation complete"
19 | }
20 | }
21 | }
22 | },
23 | {
24 | "type": "npm",
25 | "script": "test",
26 | "isBackground": true,
27 | "problemMatcher": {
28 | "owner": "typescript",
29 | "pattern": "$tsc",
30 | "background": {
31 | "activeOnStart": true,
32 | "beginsPattern": {
33 | "regexp": "(.*?)"
34 | },
35 | "endsPattern": {
36 | "regexp": "bundle generation complete"
37 | }
38 | }
39 | }
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2011-2022 Hakim El Hattab, http://hakim.se, and reveal.js contributors
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
--------------------------------------------------------------------------------
/first-ng-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "compileOnSave": false,
5 | "compilerOptions": {
6 | "outDir": "./dist/out-tsc",
7 | "strict": true,
8 | "noImplicitOverride": true,
9 | "noPropertyAccessFromIndexSignature": true,
10 | "noImplicitReturns": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "skipLibCheck": true,
13 | "isolatedModules": true,
14 | "esModuleInterop": true,
15 | "sourceMap": true,
16 | "declaration": false,
17 | "experimentalDecorators": true,
18 | "moduleResolution": "bundler",
19 | "importHelpers": true,
20 | "target": "ES2022",
21 | "module": "ES2022",
22 | "lib": [
23 | "ES2022",
24 | "dom"
25 | ]
26 | },
27 | "angularCompilerOptions": {
28 | "enableI18nLegacyMessageIdFormat": false,
29 | "strictInjectionParameters": true,
30 | "strictInputAccessModifiers": true,
31 | "strictTemplates": true
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/first-ng-app/README.md:
--------------------------------------------------------------------------------
1 | # FirstNgApp
2 |
3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.2.7.
4 |
5 | ## Development server
6 |
7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
8 |
9 | ## Code scaffolding
10 |
11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
12 |
13 | ## Build
14 |
15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
16 |
17 | ## Running unit tests
18 |
19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
20 |
21 | ## Running end-to-end tests
22 |
23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
24 |
25 | ## Further help
26 |
27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
28 |
--------------------------------------------------------------------------------
/first-ng-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "first-ng-app",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "watch": "ng build --watch --configuration development",
9 | "test": "ng test"
10 | },
11 | "private": true,
12 | "dependencies": {
13 | "@angular/animations": "^18.2.0",
14 | "@angular/common": "^18.2.0",
15 | "@angular/compiler": "^18.2.0",
16 | "@angular/core": "^18.2.0",
17 | "@angular/forms": "^18.2.0",
18 | "@angular/platform-browser": "^18.2.0",
19 | "@angular/platform-browser-dynamic": "^18.2.0",
20 | "@angular/router": "^18.2.0",
21 | "rxjs": "~7.8.0",
22 | "tslib": "^2.3.0",
23 | "zone.js": "~0.14.10"
24 | },
25 | "devDependencies": {
26 | "@angular-devkit/build-angular": "^18.2.7",
27 | "@angular/cli": "^18.2.7",
28 | "@angular/compiler-cli": "^18.2.0",
29 | "@types/jasmine": "~5.1.0",
30 | "jasmine-core": "~5.2.0",
31 | "karma": "~6.4.0",
32 | "karma-chrome-launcher": "~3.2.0",
33 | "karma-coverage": "~2.2.0",
34 | "karma-jasmine": "~5.1.0",
35 | "karma-jasmine-html-reporter": "~2.1.0",
36 | "typescript": "~5.5.2"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | Slides - Code with Ahsan
10 |
11 |
12 |
16 |
17 |
23 |
24 |
25 |
29 |
30 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/scripts/addIdsToSlide.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const { v4: uuidv4 } = require('uuid');
4 | const targetFile = process.argv[2];
5 |
6 | if (!targetFile) {
7 | console.log('Error: target file not provided\nUse `npm run addIdsToSlide ./path/to/slides.md`');
8 | process.exit(1);
9 | }
10 |
11 | const filePath = path.resolve(process.cwd(), targetFile);
12 |
13 | const addIdToSections = async (filePath) => {
14 | console.log(`Adding IDs to file ${filePath}\n`);
15 | try {
16 | let data = fs.readFileSync(filePath, 'utf8');
17 |
18 | const separators = [';VS;', ';HS;'];
19 |
20 | separators.forEach(sep => {
21 | // Break into sections
22 | const sections = data.split(new RegExp(`\n${sep}[\r\n]`));
23 | const updatedSections = sections.map(section => {
24 | if(!section.includes('\n${section}`;
26 | }
27 | // TODO: check duplicate IDs
28 | return section;
29 | });
30 |
31 | // Join the sections back together
32 | data = updatedSections.join(`\n${sep}\n`);
33 | });
34 |
35 | // Write the new data to file
36 | fs.writeFileSync(filePath, data, 'utf8');
37 | console.log('IDs successfully added!');
38 | } catch (err) {
39 | console.error('An error occurred:', err);
40 | }
41 | };
42 |
43 | // Usage
44 | addIdToSections(filePath);
45 |
--------------------------------------------------------------------------------
/profiles/ahsan.md:
--------------------------------------------------------------------------------
1 | ### Who Am I?
2 |
3 |
4 |
5 |

6 |
7 |
GDE in Angular
8 |

9 |
Software Architect at Scania Group
10 |
11 |
12 |
13 |

14 |
15 |
16 |
17 |
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Angular in 90-ish minutes
2 |
3 | Ever wanted to learn Angular quickly? Well, this repository should help. This repository is created alongside the following video tutorial to teach the most important (& core) concepts of Angular:
4 |
5 |
6 |
7 |
8 |
9 | And if you're looking for a comprehensive guide to master Angular's new reactivity model, check out my book:
10 |
11 |
12 |
13 |
14 |
15 | > From a Google-Awarded Expert, this is your definitive guide to ending state management headaches and building lightning-fast Angular applications.
16 |
17 | The repository contains both the slides shown in the video, and the application we built during the video as well.
18 |
19 | ## Watching the slides
20 |
21 | The slides are deployed [here](https://ahsanayaz.github.io/angular-in-90ish/).
22 |
23 | ## Running the slides locally
24 | - Clone this repository
25 | - `npm install`
26 | - `npm run dev`
27 |
28 |
29 | ## Running the app
30 |
31 | To run the app we built in the video tutorial (the final state):
32 | - Clone this repository if you haven't
33 | - `cd first-ng-app`
34 | - `npm install`
35 | - `npm start`
36 | - Navigate to [localhost:4200](http://localhost:4200)
37 |
--------------------------------------------------------------------------------
/first-ng-app/src/app/todos/todos.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, inject, OnInit, signal } from '@angular/core';
2 | import { TodosService } from '../services/todos.service';
3 | import { Todo } from '../model/todo.type';
4 | import { catchError } from 'rxjs';
5 | import { TodoItemComponent } from '../components/todo-item/todo-item.component';
6 | import { FormsModule } from '@angular/forms';
7 | import { FilterTodosPipe } from '../pipes/filter-todos.pipe';
8 |
9 | @Component({
10 | selector: 'app-todos',
11 | standalone: true,
12 | imports: [TodoItemComponent, FormsModule, FilterTodosPipe],
13 | templateUrl: './todos.component.html',
14 | styleUrl: './todos.component.scss',
15 | })
16 | export class TodosComponent implements OnInit {
17 | todoService = inject(TodosService);
18 | todoItems = signal>([]);
19 | searchTerm = signal('');
20 |
21 | ngOnInit(): void {
22 | this.todoService
23 | .getTodosFromApi()
24 | .pipe(
25 | catchError((err) => {
26 | console.log(err);
27 | throw err;
28 | })
29 | )
30 | .subscribe((todos) => {
31 | this.todoItems.set(todos);
32 | });
33 | }
34 |
35 | updateTodoItem(todoItem: Todo) {
36 | this.todoItems.update((todos) => {
37 | return todos.map((todo) => {
38 | if (todo.id === todoItem.id) {
39 | return {
40 | ...todo,
41 | completed: !todo.completed,
42 | };
43 | }
44 | return todo;
45 | });
46 | });
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: Deploy static content to Pages
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ['main']
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow one concurrent deployment
19 | concurrency:
20 | group: 'pages'
21 | cancel-in-progress: true
22 |
23 | jobs:
24 | # Single deploy job since we're just deploying
25 | deploy:
26 | environment:
27 | name: github-pages
28 | url: ${{ steps.deployment.outputs.page_url }}
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Checkout
32 | uses: actions/checkout@v4
33 | - name: Set up Node
34 | uses: actions/setup-node@v4
35 | with:
36 | node-version: 20
37 | cache: 'npm'
38 | - name: Install dependencies
39 | run: npm ci
40 | - name: Build
41 | run: npm run build
42 | - name: Setup Pages
43 | uses: actions/configure-pages@v4
44 | - name: Upload artifact
45 | uses: actions/upload-pages-artifact@v3
46 | with:
47 | # Upload dist folder
48 | path: './dist/angular-in-90ish'
49 | - name: Deploy to GitHub Pages
50 | id: deployment
51 | uses: actions/deploy-pages@v4
52 |
--------------------------------------------------------------------------------
/content/slides.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Reveal Multi Slides
7 |
8 |
9 |
13 |
14 |
15 |
20 |
21 |
25 |
26 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/css/slides.scss:
--------------------------------------------------------------------------------
1 | .introduction {
2 | display: flex;
3 | margin-top: 60px;
4 | align-items: center;
5 | justify-content: space-between;
6 | gap: 30px;
7 | &__left {
8 | display: flex;
9 | gap: 16px;
10 | &__avatar {
11 | border-radius: 50%;
12 | width: 160px;
13 | height: 160px;
14 | }
15 | &__info {
16 | font-size: 24px;
17 | }
18 | }
19 | &__right {
20 | display: flex;
21 | gap: 16px;
22 | flex-direction: column;
23 | align-items: center;
24 | &__gde {
25 | width: 200px;
26 | }
27 | &__ng-book {
28 | width: 200px;
29 | }
30 | }
31 | }
32 |
33 | .footer {
34 | margin-top: 60px;
35 | display: flex;
36 | justify-content: space-between;
37 | align-items: center;
38 | font-size: 24px;
39 | }
40 |
41 | img.meme {
42 | max-height: 600px !important;
43 | }
44 |
45 | .reveal .slide-number {
46 | position: absolute;
47 | display: block;
48 | right: 8px;
49 | bottom: 8px;
50 | z-index: 31;
51 | font-family: 'Nunito', Helvetica, sans-serif;
52 | font-size: 12px;
53 | line-height: 1;
54 | color: #fff;
55 | background-color: rgba(0, 0, 0, 0.4) !important;
56 | padding: 5px;
57 | }
58 |
59 | .reveal .slide-background {
60 | display: none;
61 | position: absolute;
62 | width: 100%;
63 | height: 100%;
64 | opacity: 0;
65 | visibility: hidden;
66 | overflow: hidden;
67 |
68 | background-color: rgba(0, 0, 0, 0);
69 |
70 | transition: all 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985);
71 | }
72 |
73 | :root {
74 | --r-background-color: #000;
75 | --r-main-font: 'Nunito', Helvetica, sans-serif;
76 | --r-heading-font: 'Nunito', Helvetica, sans-serif;
77 | --r-heading-text-transform: 'unset';
78 | }
79 |
80 | .reveal li code {
81 | color: yellow;
82 | }
83 |
--------------------------------------------------------------------------------
/content/static-slides.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Static Slides
7 |
8 |
9 |
10 |
14 |
15 |
16 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/scripts/extractSlideData.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | const listFilesInDirectory = (directory, fileNames) => {
5 | const files = fs.readdirSync(directory);
6 | files.forEach((file) => {
7 | const filePath = path.join(directory, file);
8 | if (fs.statSync(filePath).isDirectory()) {
9 | listFilesInDirectory(filePath, fileNames);
10 | } else {
11 | fileNames.push(filePath);
12 | }
13 | });
14 | };
15 |
16 | const extractTitleFromMarkdown = (path) => {
17 | try {
18 | const data = fs.readFileSync(path, 'utf8');
19 | const headingRegex = /^#{1,2}\s+(.*)/;
20 | const lines = data.split('\n');
21 | for (let line of lines) {
22 | const match = line.match(headingRegex);
23 | if (match) {
24 | return match[1].trim(); // Extracts the first Markdown heading
25 | }
26 | }
27 | return ''; // Return an empty string if no heading is found
28 | } catch (err) {
29 | console.log(err.message);
30 | process.exit(1);
31 | }
32 | };
33 |
34 | const extractSlideData = (folderName) => {
35 | try {
36 | const talksPath = path.resolve(folderName);
37 | const files = fs.readdirSync(talksPath);
38 |
39 | const markdownFilter = /\.md$/;
40 | const folderFilter = /\./;
41 |
42 | let content = [];
43 | files.forEach((file) => {
44 | if (!folderFilter.test(file)) {
45 | const fileNames = [];
46 | listFilesInDirectory(path.join(talksPath, file), fileNames);
47 |
48 | return fileNames.forEach((file) => {
49 | if (markdownFilter.test(file)) {
50 | const title = extractTitleFromMarkdown(file);
51 | content.push({
52 | link: file.replace(talksPath, '').substring(1),
53 | title,
54 | });
55 | }
56 | });
57 | }
58 |
59 | if (markdownFilter.test(file)) {
60 | const title = extractTitleFromMarkdown(path.join(talksPath, file));
61 | content.push({ link: file, title });
62 | }
63 | });
64 |
65 | const jsonTalks = JSON.stringify(content);
66 | return jsonTalks;
67 | } catch (err) {
68 | console.log(err);
69 | process.exit(1);
70 | }
71 | };
72 |
73 | const saveSlideData = () => {
74 | try {
75 | const jsonTalks = extractSlideData('content');
76 | const dataPath = path.resolve(path.join('data', 'slides.json'));
77 | fs.writeFileSync(dataPath, jsonTalks);
78 | } catch (err) {
79 | console.log(err);
80 | process.exit(1);
81 | }
82 | };
83 |
84 | saveSlideData();
85 |
86 | module.exports = {
87 | extractTitleFromMarkdown,
88 | extractSlideData,
89 | listFilesInDirectory,
90 | };
91 |
--------------------------------------------------------------------------------
/first-ng-app/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "first-ng-app": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "scss"
11 | }
12 | },
13 | "root": "",
14 | "sourceRoot": "src",
15 | "prefix": "app",
16 | "architect": {
17 | "build": {
18 | "builder": "@angular-devkit/build-angular:application",
19 | "options": {
20 | "outputPath": "dist/first-ng-app",
21 | "index": "src/index.html",
22 | "browser": "src/main.ts",
23 | "polyfills": ["zone.js"],
24 | "tsConfig": "tsconfig.app.json",
25 | "inlineStyleLanguage": "scss",
26 | "assets": [
27 | {
28 | "glob": "**/*",
29 | "input": "public"
30 | }
31 | ],
32 | "styles": ["src/styles.scss"],
33 | "scripts": []
34 | },
35 | "configurations": {
36 | "production": {
37 | "budgets": [
38 | {
39 | "type": "initial",
40 | "maximumWarning": "500kB",
41 | "maximumError": "1MB"
42 | },
43 | {
44 | "type": "anyComponentStyle",
45 | "maximumWarning": "2kB",
46 | "maximumError": "4kB"
47 | }
48 | ],
49 | "outputHashing": "all"
50 | },
51 | "development": {
52 | "optimization": false,
53 | "extractLicenses": false,
54 | "sourceMap": true
55 | }
56 | },
57 | "defaultConfiguration": "production"
58 | },
59 | "serve": {
60 | "builder": "@angular-devkit/build-angular:dev-server",
61 | "configurations": {
62 | "production": {
63 | "buildTarget": "first-ng-app:build:production"
64 | },
65 | "development": {
66 | "buildTarget": "first-ng-app:build:development"
67 | }
68 | },
69 | "defaultConfiguration": "development"
70 | },
71 | "extract-i18n": {
72 | "builder": "@angular-devkit/build-angular:extract-i18n"
73 | },
74 | "test": {
75 | "builder": "@angular-devkit/build-angular:karma",
76 | "options": {
77 | "polyfills": ["zone.js", "zone.js/testing"],
78 | "tsConfig": "tsconfig.spec.json",
79 | "inlineStyleLanguage": "scss",
80 | "assets": [
81 | {
82 | "glob": "**/*",
83 | "input": "public"
84 | }
85 | ],
86 | "styles": ["src/styles.scss"],
87 | "scripts": []
88 | }
89 | }
90 | }
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-in-90ish",
3 | "version": "4.4.0",
4 | "description": "",
5 | "license": "MIT",
6 | "scripts": {
7 | "dev": "npm run extract && webpack serve",
8 | "build": "npm run extract && cross-env NODE_ENV=production webpack --mode production",
9 | "extract": "node scripts/extractSlideData.js",
10 | "lint": "eslint ./js/**/* --fix",
11 | "serve:prod": "npm run build && npx http-server ./dist -c-1 -o /angular-in-90ish/"
12 | },
13 | "author": {
14 | "name": "Muhammad Ahsan Ayaz",
15 | "email": "ahsan.ubitian@gmail.com",
16 | "web": "https://codewithahsan.dev"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git://github.com/ahsanayaz/slides.git"
21 | },
22 | "engines": {
23 | "node": ">=18.0.0"
24 | },
25 | "keywords": [
26 | "reveal",
27 | "slides",
28 | "presentation"
29 | ],
30 | "devDependencies": {
31 | "@babel/core": "^7.24.4",
32 | "@babel/eslint-parser": "^7.14.3",
33 | "@babel/preset-env": "^7.24.4",
34 | "autoprefixer": "^10.4.19",
35 | "babel-loader": "^9.1.3",
36 | "babel-plugin-transform-html-import-to-string": "0.0.1",
37 | "clean-webpack-plugin": "^4.0.0",
38 | "colors": "^1.4.0",
39 | "copy-webpack-plugin": "^12.0.2",
40 | "core-js": "^3.12.1",
41 | "cross-env": "^7.0.3",
42 | "css-loader": "^7.1.1",
43 | "css-minimizer-webpack-plugin": "^6.0.0",
44 | "eslint": "^8.57.0",
45 | "eslint-plugin-react": "^7.34.1",
46 | "eslint-webpack-plugin": "^4.1.0",
47 | "fitty": "^2.3.0",
48 | "gh-pages": "^4.0.0",
49 | "highlight.js": "^11.9.0",
50 | "html-webpack-plugin": "^5.6.0",
51 | "jest": "^29.7.0",
52 | "marked": "^4.0.12",
53 | "mini-css-extract-plugin": "^2.8.1",
54 | "postcss": "^8.4.38",
55 | "postcss-loader": "^8.1.1",
56 | "prettier": "^2.8.0",
57 | "puppeteer": "^22.6.4",
58 | "sass": "^1.75.0",
59 | "sass-loader": "^14.2.0",
60 | "style-loader": "^4.0.0",
61 | "terser-webpack-plugin": "^5.3.10",
62 | "webpack": "^5.91.0",
63 | "webpack-cli": "^5.1.4",
64 | "webpack-dev-server": "^5.0.4"
65 | },
66 | "browserslist": "> 2%, not dead",
67 | "eslintConfig": {
68 | "env": {
69 | "browser": true,
70 | "es6": true
71 | },
72 | "parser": "@babel/eslint-parser",
73 | "parserOptions": {
74 | "sourceType": "module",
75 | "allowImportExportEverywhere": true,
76 | "requireConfigFile": false
77 | },
78 | "globals": {
79 | "module": false,
80 | "console": false,
81 | "unescape": false,
82 | "define": false,
83 | "exports": false
84 | },
85 | "rules": {
86 | "curly": 0,
87 | "eqeqeq": 2,
88 | "wrap-iife": [
89 | 2,
90 | "any"
91 | ],
92 | "no-use-before-define": [
93 | 2,
94 | {
95 | "functions": false
96 | }
97 | ],
98 | "new-cap": 2,
99 | "no-caller": 2,
100 | "dot-notation": 0,
101 | "no-eq-null": 2,
102 | "no-unused-expressions": 0
103 | }
104 | },
105 | "dependencies": {
106 | "reveal.js": "^5.1.0",
107 | "uuid": "^9.0.1"
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
5 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
6 | const TerserPlugin = require('terser-webpack-plugin');
7 | const CopyWebpackPlugin = require('copy-webpack-plugin');
8 | const ESLintPlugin = require('eslint-webpack-plugin');
9 | const fs = require('fs');
10 |
11 | const BASE_HREF = 'angular-in-90ish';
12 |
13 | // Function to generate HtmlWebpackPlugin instances for each HTML file
14 | function generateHtmlPlugins(templateDir) {
15 | const templateFiles = fs.readdirSync(path.resolve(__dirname, templateDir));
16 | return templateFiles
17 | .filter((fileName) => fileName.endsWith('html'))
18 | .map((item) => {
19 | const parts = item.split('.');
20 | const name = parts[0];
21 | const extension = parts[1];
22 | return new HtmlWebpackPlugin({
23 | filename: `${name}.html`,
24 | template: path.resolve(
25 | __dirname,
26 | `${templateDir}/${name}.${extension}`
27 | ),
28 | base: process.env.NODE_ENV !== 'production' ? '/' : `/${BASE_HREF}/`,
29 | inject: true,
30 | minify: false,
31 | });
32 | });
33 | }
34 |
35 | const htmlPlugins = generateHtmlPlugins('./content');
36 |
37 | module.exports = {
38 | entry: './js/slides.js',
39 | output: {
40 | filename: 'bundle.[contenthash].js',
41 | path: path.resolve(
42 | __dirname,
43 | process.env.NODE_ENV === 'production' ? `dist/${BASE_HREF}` : 'dist'
44 | ),
45 | },
46 | mode: 'development', // Change to 'production' when ready to deploy
47 | devtool: 'source-map',
48 | plugins: [
49 | new CleanWebpackPlugin(),
50 | new HtmlWebpackPlugin({
51 | template: './index.html',
52 | inject: false,
53 | minify: false,
54 | }),
55 | ...htmlPlugins,
56 | new MiniCssExtractPlugin({
57 | filename: 'styles/[name].[contenthash].css',
58 | }),
59 | new CopyWebpackPlugin({
60 | patterns: [
61 | { from: 'content', to: 'content' },
62 | { from: 'data', to: 'data' },
63 | { from: 'main.js', to: 'main.js' },
64 | { from: 'profiles', to: 'profiles' },
65 | { from: 'assets', to: 'assets' },
66 | ],
67 | }),
68 | new ESLintPlugin(),
69 | ],
70 | module: {
71 | rules: [
72 | {
73 | test: /\.js$/,
74 | exclude: /node_modules/,
75 | use: 'babel-loader',
76 | },
77 | {
78 | test: /\.scss$/,
79 | use: [
80 | MiniCssExtractPlugin.loader,
81 | 'css-loader',
82 | 'postcss-loader',
83 | 'sass-loader',
84 | ],
85 | },
86 | {
87 | test: /\.css$/,
88 | use: [MiniCssExtractPlugin.loader, 'css-loader'],
89 | },
90 | {
91 | test: /\.(png|jpeg|jpg|gif|svg)$/i,
92 | type: 'asset/resource',
93 | },
94 | ],
95 | },
96 | optimization: {
97 | minimize: true,
98 | minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
99 | },
100 | devServer: {
101 | static: {
102 | directory: path.join(__dirname, 'dist'),
103 | watch: true,
104 | },
105 | watchFiles: ['content/**/*', 'css/**/*', 'profiles/*'],
106 | open: true,
107 | port: 8000,
108 | hot: true,
109 | },
110 | };
111 |
--------------------------------------------------------------------------------
/assets/images/app-mockup.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/images/components-1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/content/angular-in-90ish.md:
--------------------------------------------------------------------------------
1 | # Angular in 90-ish minutes
2 |
3 |
4 | #### Muhammad Ahsan Ayaz
5 |
6 | ---
7 |
8 | ## What you should know so far?
9 |
10 | - HTML
11 | - JavaScript (or TypeScript)
12 | - CSS (a bit)
13 | - Git
14 | - Basic programming concepts (variables, loops, functions, conditionals)
15 |
16 | ---
17 |
18 | ## Which tools do we need?
19 |
20 | - [VSCode](https://code.visualstudio.com/Download)
21 | - [NodeJS](https://nodejs.org/en/download/prebuilt-installer)
22 | - [Git](https://git-scm.com/downloads)
23 | - [Angular CLI](https://www.npmjs.com/package/@angular/cli)
24 | - [Angular Language Server extension](https://marketplace.visualstudio.com/items?itemName=Angular.ng-template)
25 |
26 | ---
27 |
28 | ## What is Angular
29 |
30 | - Web Applications framework for Single Page Apps (SPA)
31 | - Built by Google
32 | - Has a huge commmunity
33 |
34 | ---
35 |
36 | 
37 |
38 | ---
39 |
40 | ## Angular Cookbook
41 |
42 | - Winning Component Communication
43 | - Working with Angular Directives and Built-In Control Flow
44 | - The Magic of Dependency Injection in Angular
45 | - Understanding Angular Animations
46 | - Angular and RxJS – Awesomeness Combined
47 | - Reactive State Management with NgRx
48 | - Understanding Angular Navigation and Routing
49 |
50 | --
51 |
52 | ## Angular Cookbook
53 |
54 | - Mastering Angular Forms
55 | - Angular and the Angular CDK
56 | - Writing Unit Tests in Angular with Jest
57 | - E2E Tests in Angular with Cypress
58 | - Performance Optimization in Angular
59 | - Building PWAs with Angular
60 |
61 | ---
62 |
63 | ## Benefits of Angular
64 |
65 | - Faster development
66 | - Faster code generation (CLI)
67 | - Unit-tests ready
68 | - Opinionated
69 | - Makes it easy to switch companies and teams
70 | - Code reusability
71 |
72 | ---
73 |
74 | ## Angular vs React
75 | ### Myths about Angular
76 |
77 | --
78 |
79 | ### Angular vs React
80 |
81 |
82 | #### Angular
83 | - Is a framework
84 | - Has a built-in CLI
85 | - Has tools and packages included for small-medium scale apps
86 | - Is opinionated (better code style consistency)
87 |
88 | --
89 | ### Angular vs React
90 |
91 |
92 | #### React
93 | - Is a library
94 | - Does not have a CLI
95 | - Requires you to install additional packages even for small scale apps
96 |
97 |
98 | --
99 | ## Myths about Angular
100 |
101 | - It is hard to learn
102 | - Will change significantly on every update
103 | - Angular is slow ([Not really](https://krausest.github.io/js-framework-benchmark/2024/table_chrome_129.0.6668.58.html))
104 |
105 |
106 | - Angular has a huge bundle size
107 |
108 | ---
109 |
110 | ## Angular Core Concepts
111 | - Components, Services
112 | - Directive, Pipes
113 | - Data-Binding, Event Handlers
114 | - Http Module, Forms Module
115 | - Routing, Animations
116 | - Testing, Building for production
117 |
118 | ---
119 |
120 | ## Creating an Angular app
121 |
122 | ```bash
123 | # install the @angular/cli
124 | npm install -g @angular/cli
125 |
126 | # check cli version
127 | ng --version
128 |
129 | # create an app
130 | ng new first-ng-app # optionally use --dry-run
131 |
132 | # create an app with some configuration
133 | ng new first-ng-app --inline-style --inline-template
134 | ```
135 |
136 | ---
137 |
138 | ## Angular Components
139 |
140 | Example
141 |
142 | 
143 |
144 | --
145 |
146 | 
147 |
148 | --
149 |
150 | #### Creating a component
151 | ```bash
152 | ng g c header # short form
153 | ng generate component header # full form
154 | # creates inside the `src/app` folder
155 |
156 |
157 | # OR (in a nested directory)
158 | ng g c components/header
159 | # creates HeaderComponent
160 | # inside the `src/app/components` folder
161 |
162 | ng g c home
163 | # creates the HomeComponent
164 | ```
165 |
166 | --
167 |
168 | ## Let's style the header and home
169 |
170 | ---
171 |
172 | ## Angular Data-Binding
173 |
174 | Binding data between the TypeScript class of the component, and the component's template.
175 |
176 | --
177 |
178 | ### Data Binding with Modern Angular (with Signals)
179 |
180 | ```ts
181 | import { Component, signal } from '@angular/core';
182 | @Component({
183 | ...,
184 | template: `
185 | Here's my var's value: {{myVar()}}
186 | `
187 | })
188 | class MyComponent {
189 | myVar = signal('some value');
190 | }
191 | ```
192 |
193 | more on signals later...
194 |
195 | --
196 |
197 | ### Data Binding without Signals (traditional way)
198 |
199 | ```ts
200 | import { Component } from '@angular/core';
201 | @Component({
202 | ...,
203 | template: `
204 | Here's my var's value: {{myVar}}
205 | `
206 | })
207 | class MyComponent {
208 | myVar = 'some value';
209 | }
210 | ```
211 |
212 | --
213 |
214 | 
215 |
216 | --
217 |
218 | ### Creating `GreetingComponent`
219 |
220 | ```bash
221 | ng g c components/greeting
222 | # generates in `src/app/components`
223 | ```
224 |
225 | --
226 |
227 | #### Passing data from parent to child component via Inputs
228 |
229 | We'll pass the greeting message from the AppComponent
230 |
231 | ---
232 |
233 | ### Event listeners in Angular
234 |
235 | ```html
236 |
237 |
238 | ```
239 |
240 | ```ts
241 | class MyComponent {
242 | keyUpHandler() {
243 | console.log('user typed something in the input');
244 | }
245 | }
246 | ```
247 |
248 | --
249 |
250 | ### Event listeners in Angular
251 |
252 | ```html
253 |
254 |
255 | ```
256 |
257 | ```ts
258 | class MyComponent {
259 | keyUpHandler(event: KeyboardEvent) {
260 | console.log(`user pressed the ${event.key} key`);
261 | }
262 | }
263 | ```
264 |
265 | --
266 |
267 | ## Let's create a counter component
268 |
269 | ```bash
270 | ng g c components/counter
271 | ```
272 |
273 | ---
274 |
275 | ## Routing in Angular
276 |
277 | --
278 |
279 | Angular is a single page application. Using routes, you can still define different pages that the user can navigate to.
280 |
281 | The browser only loads the bundles related to the route user has accessed.
282 |
283 | This significantly improves the performance of the app, and user experience.
284 |
285 | --
286 |
287 | ## Let's create a new route
288 |
289 | --
290 |
291 | 
292 |
293 | --
294 |
295 | #### Create another component (as a page) for the route
296 |
297 | ```bash
298 | ng g c todos
299 | # this will be the page for todos' list
300 | ```
301 |
302 | --
303 |
304 | 
305 |
306 | --
307 |
308 | #### Create a component for each todo item
309 |
310 | ```bash
311 | ng g c components/todo-item
312 | ```
313 |
314 | ---
315 |
316 | ## Angular Services
317 |
318 | --
319 |
320 | #### Angular Services
321 |
322 | Angular Services are used to encapsulate data, making HTTP calls, or performing any task that is not related directly to data rendering (in my opinion).
323 |
324 | --
325 |
326 | #### Creating an Angular Service
327 |
328 | ```bash
329 | ng g service services/todos
330 | # creates todos.service.ts inside `src/app/services`
331 | ```
332 |
333 | --
334 |
335 | #### Example of serving data from an Angular Service
336 |
337 | --
338 |
339 | ## Making HTTP calls with Angular Services
340 |
341 | - Provide HTTP module/providers in the app config using `provideHttpClient()`
342 | - Inject the `HttpClient` service
343 | - Use the `http` methods
344 |
345 | ---
346 |
347 | ## Angular Directives
348 |
349 | Angular Directives allow you to add additional behavior to elements in our Angular applications.
350 |
351 | --
352 |
353 | #### Types of Angular Directives
354 |
355 | - Components
356 | - Attribute directives
357 | - Structural directives
358 |
359 | --
360 |
361 | #### Let's create an Angular directive for completed todos
362 |
363 | ```bash
364 | ng g directive directives/highlight-completed-todo
365 | ```
366 |
367 | ---
368 |
369 | ## Angular Pipes
370 |
371 | --
372 |
373 | Angular pipes are used to transform data right in the templates
374 |
375 | --
376 |
377 | ## [Built-in Angular pipes](https://angular.dev/guide/templates/pipes)
378 |
379 | --
380 |
381 | ## Let's create a todos filter pipe
382 |
383 | ```bash
384 | ng g pipe pipes/filter-todos
385 | ```
386 |
387 | ---
388 |
389 | ## Thank you!
390 |
--------------------------------------------------------------------------------
/assets/images/home-with-nested-components.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/images/todos-route.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/images/todo-item.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------