diff --git a/apps/rxjs-to-signal/.eslintrc.json b/apps/rxjs-to-signal/.eslintrc.json
new file mode 100644
index 0000000..b428c22
--- /dev/null
+++ b/apps/rxjs-to-signal/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "app",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "app",
+ "style": "kebab-case"
+ }
+ ]
+ },
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/apps/rxjs-to-signal/README.md b/apps/rxjs-to-signal/README.md
new file mode 100644
index 0000000..37ff9a2
--- /dev/null
+++ b/apps/rxjs-to-signal/README.md
@@ -0,0 +1,32 @@
+
migrate application to signals
+
+> Author: Thomas Laforge
+
+
+
+### Information
+
+### Statement
+
+### Step 1
+
+### Step 2
+
+### Constraints:
+
+### Submitting your work
+
+1. Fork the project
+2. clone it
+3. npm ci
+4. `npx nx serve rxjs-to-signal`
+5. _...work on it_
+6. Commit your work
+7. Submit a PR with a title beginning with **Answer:30** that I will review and other dev can review.
+
+
+
+
+
+
+_You can ask any question on_
diff --git a/apps/rxjs-to-signal/jest.config.ts b/apps/rxjs-to-signal/jest.config.ts
new file mode 100644
index 0000000..e0adc03
--- /dev/null
+++ b/apps/rxjs-to-signal/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+export default {
+ displayName: 'rxjs-to-signal',
+ preset: '../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../coverage/apps/rxjs-to-signal',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/apps/rxjs-to-signal/project.json b/apps/rxjs-to-signal/project.json
new file mode 100644
index 0000000..741d5ed
--- /dev/null
+++ b/apps/rxjs-to-signal/project.json
@@ -0,0 +1,98 @@
+{
+ "name": "rxjs-to-signal",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/rxjs-to-signal/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/rxjs-to-signal",
+ "index": "apps/rxjs-to-signal/src/index.html",
+ "main": "apps/rxjs-to-signal/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/rxjs-to-signal/tsconfig.app.json",
+ "assets": [
+ "apps/rxjs-to-signal/src/favicon.ico",
+ "apps/rxjs-to-signal/src/assets"
+ ],
+ "styles": [
+ "apps/rxjs-to-signal/src/styles.scss",
+ "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css"
+ ],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "browserTarget": "rxjs-to-signal:build:production"
+ },
+ "development": {
+ "browserTarget": "rxjs-to-signal:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "browserTarget": "rxjs-to-signal:build"
+ }
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": [
+ "apps/rxjs-to-signal/**/*.ts",
+ "apps/rxjs-to-signal/**/*.html"
+ ]
+ }
+ },
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "apps/rxjs-to-signal/jest.config.ts",
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "codeCoverage": true
+ }
+ }
+ }
+ }
+}
diff --git a/apps/rxjs-to-signal/src/app/app.component.ts b/apps/rxjs-to-signal/src/app/app.component.ts
new file mode 100644
index 0000000..f63192e
--- /dev/null
+++ b/apps/rxjs-to-signal/src/app/app.component.ts
@@ -0,0 +1,11 @@
+import { Component } from '@angular/core';
+import { RouterOutlet } from '@angular/router';
+
+@Component({
+ standalone: true,
+ imports: [RouterOutlet],
+ selector: 'app-root',
+ template: ``,
+ styles: [''],
+})
+export class AppComponent {}
diff --git a/apps/rxjs-to-signal/src/app/app.config.ts b/apps/rxjs-to-signal/src/app/app.config.ts
new file mode 100644
index 0000000..cd49643
--- /dev/null
+++ b/apps/rxjs-to-signal/src/app/app.config.ts
@@ -0,0 +1,29 @@
+import { provideHttpClient } from '@angular/common/http';
+import { ApplicationConfig } from '@angular/core';
+import { provideAnimations } from '@angular/platform-browser/animations';
+import { provideRouter, withComponentInputBinding } from '@angular/router';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideHttpClient(),
+ provideAnimations(),
+ provideRouter(
+ [
+ {
+ path: '',
+ pathMatch: 'full',
+ loadComponent: () => import('./list/photos.component'),
+ },
+ {
+ path: 'detail',
+ loadComponent: () => import('./detail/detail.component'),
+ },
+ {
+ path: '**',
+ redirectTo: '',
+ },
+ ],
+ withComponentInputBinding()
+ ),
+ ],
+};
diff --git a/apps/rxjs-to-signal/src/app/detail/detail.component.ts b/apps/rxjs-to-signal/src/app/detail/detail.component.ts
new file mode 100644
index 0000000..63cf622
--- /dev/null
+++ b/apps/rxjs-to-signal/src/app/detail/detail.component.ts
@@ -0,0 +1,48 @@
+import { JsonPipe, NgFor, NgIf } from '@angular/common';
+import { Component, Input } from '@angular/core';
+import { ReactiveFormsModule } from '@angular/forms';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatProgressBarModule } from '@angular/material/progress-bar';
+import { LetDirective } from '@ngrx/component';
+import { Photo } from '../photo.model';
+
+@Component({
+ selector: 'app-photos',
+ standalone: true,
+ imports: [
+ ReactiveFormsModule,
+ MatFormFieldModule,
+ MatProgressBarModule,
+ NgIf,
+ NgFor,
+ MatInputModule,
+ LetDirective,
+ JsonPipe,
+ ],
+ template: `
+
+ {{ photo | json }}
+ `,
+ // providers: [provideComponentStore(PhotoStore)],
+ host: {
+ class: 'p-5 block',
+ },
+})
+export default class DetailComponent {
+ @Input({ required: true }) photo!: Photo;
+ // store = inject(PhotoStore);
+ // readonly vm$ = this.store.vm$;
+
+ // search = new FormControl();
+
+ // ngOnInit(): void {
+ // this.store.search(
+ // this.search.valueChanges.pipe(debounceTime(300), distinctUntilChanged())
+ // );
+ // }
+
+ // trackById(index: number, photo: Photo) {
+ // return photo.id;
+ // }
+}
diff --git a/apps/rxjs-to-signal/src/app/list/photos.component.ts b/apps/rxjs-to-signal/src/app/list/photos.component.ts
new file mode 100644
index 0000000..0d8b948
--- /dev/null
+++ b/apps/rxjs-to-signal/src/app/list/photos.component.ts
@@ -0,0 +1,107 @@
+import { NgFor, NgIf } from '@angular/common';
+import { Component, OnInit, inject } from '@angular/core';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatProgressBarModule } from '@angular/material/progress-bar';
+import { RouterLinkWithHref } from '@angular/router';
+import { LetDirective } from '@ngrx/component';
+import { provideComponentStore } from '@ngrx/component-store';
+import { debounceTime, distinctUntilChanged } from 'rxjs';
+import { Photo } from '../photo.model';
+import { PhotoStore } from './photos.store';
+
+@Component({
+ selector: 'app-photos',
+ standalone: true,
+ imports: [
+ ReactiveFormsModule,
+ MatFormFieldModule,
+ MatProgressBarModule,
+ NgIf,
+ NgFor,
+ MatInputModule,
+ LetDirective,
+ RouterLinkWithHref,
+ ],
+ template: `
+ Photos
+
+
+ Search
+
+
+
+
+
+
+
+
+
+ Page :{{ vm.page }} / {{ vm.pages }}
+
+ 0; else noPhoto">
+ -
+
+
+
+
+
+
+ No Photos found. Type a search word.
+
+
+
+
+ `,
+ providers: [provideComponentStore(PhotoStore)],
+ host: {
+ class: 'p-5 block',
+ },
+})
+export default class PhotosComponent implements OnInit {
+ store = inject(PhotoStore);
+ readonly vm$ = this.store.vm$;
+
+ search = new FormControl();
+
+ ngOnInit(): void {
+ this.store.search(
+ this.search.valueChanges.pipe(debounceTime(300), distinctUntilChanged())
+ );
+ }
+
+ trackById(index: number, photo: Photo) {
+ return photo.id;
+ }
+
+ encode(photo: Photo) {
+ return encodeURIComponent(JSON.stringify(photo));
+ }
+}
diff --git a/apps/rxjs-to-signal/src/app/list/photos.store.ts b/apps/rxjs-to-signal/src/app/list/photos.store.ts
new file mode 100644
index 0000000..2d720cc
--- /dev/null
+++ b/apps/rxjs-to-signal/src/app/list/photos.store.ts
@@ -0,0 +1,118 @@
+import { Injectable, inject } from '@angular/core';
+import {
+ ComponentStore,
+ OnStateInit,
+ OnStoreInit,
+ tapResponse,
+} from '@ngrx/component-store';
+import { pipe } from 'rxjs';
+import { filter, mergeMap, tap } from 'rxjs/operators';
+import { Photo } from '../photo.model';
+import { PhotoService } from '../photos.service';
+
+export interface PhotoState {
+ photos: Photo[];
+ search: string;
+ page: number;
+ pages: number;
+ loading: boolean;
+ error: unknown;
+}
+
+const initialState: PhotoState = {
+ photos: [],
+ search: '',
+ page: 1,
+ pages: 1,
+ loading: false,
+ error: '',
+};
+
+@Injectable()
+export class PhotoStore
+ extends ComponentStore
+ implements OnStoreInit, OnStateInit
+{
+ private photoService = inject(PhotoService);
+
+ private readonly photos$ = this.select((s) => s.photos);
+ private readonly search$ = this.select((s) => s.search);
+ private readonly page$ = this.select((s) => s.page);
+ private readonly pages$ = this.select((s) => s.pages);
+ private readonly error$ = this.select((s) => s.error);
+ private readonly loading$ = this.select((s) => s.loading);
+
+ private readonly endOfPage$ = this.select(
+ this.page$,
+ this.pages$,
+ (page, pages) => page === pages
+ );
+
+ readonly vm$ = this.select(
+ {
+ photos: this.photos$,
+ page: this.page$,
+ pages: this.pages$,
+ endOfPage: this.endOfPage$,
+ loading: this.loading$,
+ error: this.error$,
+ },
+ { debounce: true }
+ );
+
+ ngrxOnStoreInit() {
+ this.setState(initialState);
+ }
+
+ ngrxOnStateInit() {
+ this.searchPhotos(
+ this.select({
+ search: this.search$,
+ page: this.page$,
+ })
+ );
+ }
+
+ readonly search = this.updater(
+ (state, search: string): PhotoState => ({
+ ...state,
+ search,
+ page: 1,
+ })
+ );
+
+ readonly nextPage = this.updater(
+ (state): PhotoState => ({
+ ...state,
+ page: state.page + 1,
+ })
+ );
+
+ readonly previousPage = this.updater(
+ (state): PhotoState => ({
+ ...state,
+ page: state.page - 1,
+ })
+ );
+
+ readonly searchPhotos = this.effect<{ search: string; page: number }>(
+ pipe(
+ filter(({ search }) => search.length >= 3),
+ tap(() => this.patchState({ loading: true, error: '' })),
+ mergeMap(({ search, page }) =>
+ this.photoService.searchPublicPhotos(search, page).pipe(
+ tapResponse(
+ ({ photos: { photo, pages } }) => {
+ this.patchState({
+ loading: false,
+ photos: photo,
+ pages,
+ });
+ },
+ (error: unknown) => this.patchState({ error, loading: false })
+ )
+ )
+ )
+ )
+ );
+}
diff --git a/apps/rxjs-to-signal/src/app/photo.model.ts b/apps/rxjs-to-signal/src/app/photo.model.ts
new file mode 100644
index 0000000..5c78a91
--- /dev/null
+++ b/apps/rxjs-to-signal/src/app/photo.model.ts
@@ -0,0 +1,10 @@
+export interface Photo {
+ id: string;
+ title: string;
+ tags: string;
+ owner: string;
+ ownername: string;
+ datetaken: string;
+ url_q: string;
+ url_m: string;
+}
diff --git a/apps/rxjs-to-signal/src/app/photos.service.ts b/apps/rxjs-to-signal/src/app/photos.service.ts
new file mode 100644
index 0000000..a7999e6
--- /dev/null
+++ b/apps/rxjs-to-signal/src/app/photos.service.ts
@@ -0,0 +1,39 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable, inject } from '@angular/core';
+import { Observable } from 'rxjs';
+import { Photo } from './photo.model';
+
+export interface FlickrAPIResponse {
+ photos: {
+ pages: number;
+ photo: Photo[];
+ };
+}
+
+@Injectable({ providedIn: 'root' })
+export class PhotoService {
+ private http = inject(HttpClient);
+
+ public searchPublicPhotos(
+ searchTerm: string,
+ page: number
+ ): Observable {
+ return this.http.get(
+ 'https://www.flickr.com/services/rest/',
+ {
+ params: {
+ tags: searchTerm,
+ method: 'flickr.photos.search',
+ format: 'json',
+ nojsoncallback: '1',
+ tag_mode: 'all',
+ media: 'photos',
+ per_page: '30',
+ page,
+ extras: 'tags,date_taken,owner_name,url_q,url_m',
+ api_key: 'c3050d39a5bb308d9921bef0e15c437d',
+ },
+ }
+ );
+ }
+}
diff --git a/apps/rxjs-to-signal/src/assets/.gitkeep b/apps/rxjs-to-signal/src/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/apps/rxjs-to-signal/src/favicon.ico b/apps/rxjs-to-signal/src/favicon.ico
new file mode 100644
index 0000000..317ebcb
Binary files /dev/null and b/apps/rxjs-to-signal/src/favicon.ico differ
diff --git a/apps/rxjs-to-signal/src/index.html b/apps/rxjs-to-signal/src/index.html
new file mode 100644
index 0000000..b072aa4
--- /dev/null
+++ b/apps/rxjs-to-signal/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ rxjs-to-signal
+
+
+
+
+
+
+
+
diff --git a/apps/rxjs-to-signal/src/main.ts b/apps/rxjs-to-signal/src/main.ts
new file mode 100644
index 0000000..514c89a
--- /dev/null
+++ b/apps/rxjs-to-signal/src/main.ts
@@ -0,0 +1,7 @@
+import { bootstrapApplication } from '@angular/platform-browser';
+import { appConfig } from './app/app.config';
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent, appConfig).catch((err) =>
+ console.error(err)
+);
diff --git a/apps/rxjs-to-signal/src/styles.scss b/apps/rxjs-to-signal/src/styles.scss
new file mode 100644
index 0000000..77e408a
--- /dev/null
+++ b/apps/rxjs-to-signal/src/styles.scss
@@ -0,0 +1,5 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* You can add global styles to this file, and also import other style files */
diff --git a/apps/rxjs-to-signal/src/test-setup.ts b/apps/rxjs-to-signal/src/test-setup.ts
new file mode 100644
index 0000000..15de72a
--- /dev/null
+++ b/apps/rxjs-to-signal/src/test-setup.ts
@@ -0,0 +1,2 @@
+import '@testing-library/jest-dom';
+import 'jest-preset-angular/setup-jest';
diff --git a/apps/rxjs-to-signal/tailwind.config.js b/apps/rxjs-to-signal/tailwind.config.js
new file mode 100644
index 0000000..38183db
--- /dev/null
+++ b/apps/rxjs-to-signal/tailwind.config.js
@@ -0,0 +1,14 @@
+const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind');
+const { join } = require('path');
+
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
+ ...createGlobPatternsForDependencies(__dirname),
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+};
diff --git a/apps/rxjs-to-signal/tsconfig.app.json b/apps/rxjs-to-signal/tsconfig.app.json
new file mode 100644
index 0000000..fff4a41
--- /dev/null
+++ b/apps/rxjs-to-signal/tsconfig.app.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "types": []
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/rxjs-to-signal/tsconfig.editor.json b/apps/rxjs-to-signal/tsconfig.editor.json
new file mode 100644
index 0000000..8ae117d
--- /dev/null
+++ b/apps/rxjs-to-signal/tsconfig.editor.json
@@ -0,0 +1,7 @@
+{
+ "extends": "./tsconfig.json",
+ "include": ["src/**/*.ts"],
+ "compilerOptions": {
+ "types": ["jest", "node"]
+ }
+}
diff --git a/apps/rxjs-to-signal/tsconfig.json b/apps/rxjs-to-signal/tsconfig.json
new file mode 100644
index 0000000..e01cf19
--- /dev/null
+++ b/apps/rxjs-to-signal/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ },
+ {
+ "path": "./tsconfig.editor.json"
+ }
+ ],
+ "extends": "../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/rxjs-to-signal/tsconfig.spec.json b/apps/rxjs-to-signal/tsconfig.spec.json
new file mode 100644
index 0000000..1a4817a
--- /dev/null
+++ b/apps/rxjs-to-signal/tsconfig.spec.json
@@ -0,0 +1,15 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "module": "commonjs",
+ "types": ["jest", "node", "@testing-library/jest-dom"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}