mirror of
https://github.com/Raghu-Ch/angular-challenges.git
synced 2026-02-10 12:53:03 -05:00
feat(challenge30): challenge signal
This commit is contained in:
36
apps/rxjs-to-signal/.eslintrc.json
Normal file
36
apps/rxjs-to-signal/.eslintrc.json
Normal file
@@ -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": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
32
apps/rxjs-to-signal/README.md
Normal file
32
apps/rxjs-to-signal/README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<h1>migrate application to signals</h1>
|
||||||
|
|
||||||
|
> Author: Thomas Laforge
|
||||||
|
|
||||||
|
<!-- TODO: add Information/Statement/Rules/Constraint/Steps -->
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
<a href="https://github.com/tomalaforge/angular-challenges/pulls?q=label%3A30+label%3Aanswer"><img src="https://img.shields.io/badge/-Solutions-green" alt="rxjs-to-signal"/></a>
|
||||||
|
<a href='https://github.com/tomalaforge/angular-challenges/pulls?q=label%3A30+label%3A"answer+author"'><img src="https://img.shields.io/badge/-Author solution-important" alt="rxjs-to-signal solution author"/></a>
|
||||||
|
|
||||||
|
<!-- <a href="{Blog post url}" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/-Blog post explanation-blue" alt="rxjs-to-signal blog article"/></a> -->
|
||||||
|
|
||||||
|
_You can ask any question on_ <a href="https://twitter.com/laforge_toma" target="_blank" rel="noopener noreferrer"><img src="./../../logo/twitter.svg" height=20px alt="twitter"/></a>
|
||||||
22
apps/rxjs-to-signal/jest.config.ts
Normal file
22
apps/rxjs-to-signal/jest.config.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
export default {
|
||||||
|
displayName: 'rxjs-to-signal',
|
||||||
|
preset: '../../jest.preset.js',
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||||
|
coverageDirectory: '../../coverage/apps/rxjs-to-signal',
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(ts|mjs|js|html)$': [
|
||||||
|
'jest-preset-angular',
|
||||||
|
{
|
||||||
|
tsconfig: '<rootDir>/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',
|
||||||
|
],
|
||||||
|
};
|
||||||
98
apps/rxjs-to-signal/project.json
Normal file
98
apps/rxjs-to-signal/project.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
apps/rxjs-to-signal/src/app/app.component.ts
Normal file
11
apps/rxjs-to-signal/src/app/app.component.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
imports: [RouterOutlet],
|
||||||
|
selector: 'app-root',
|
||||||
|
template: `<router-outlet />`,
|
||||||
|
styles: [''],
|
||||||
|
})
|
||||||
|
export class AppComponent {}
|
||||||
29
apps/rxjs-to-signal/src/app/app.config.ts
Normal file
29
apps/rxjs-to-signal/src/app/app.config.ts
Normal file
@@ -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()
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
48
apps/rxjs-to-signal/src/app/detail/detail.component.ts
Normal file
48
apps/rxjs-to-signal/src/app/detail/detail.component.ts
Normal file
@@ -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: `
|
||||||
|
<img src="{{ photo.url_q }}" alt="{{ photo.title }}" class="image" />
|
||||||
|
{{ 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;
|
||||||
|
// }
|
||||||
|
}
|
||||||
107
apps/rxjs-to-signal/src/app/list/photos.component.ts
Normal file
107
apps/rxjs-to-signal/src/app/list/photos.component.ts
Normal file
@@ -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: `
|
||||||
|
<h2 class="text-xl mb-2">Photos</h2>
|
||||||
|
|
||||||
|
<mat-form-field appearance="fill">
|
||||||
|
<mat-label>Search</mat-label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
matInput
|
||||||
|
[formControl]="search"
|
||||||
|
placeholder="write an article" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<ng-container *ngrxLet="vm$ as vm">
|
||||||
|
<section class="flex flex-col">
|
||||||
|
<mat-progress-bar
|
||||||
|
mode="query"
|
||||||
|
*ngIf="vm.loading"
|
||||||
|
class="mt-5"></mat-progress-bar>
|
||||||
|
<section class="flex gap-3 items-center">
|
||||||
|
<button
|
||||||
|
[disabled]="vm.page === 1"
|
||||||
|
[class.bg-gray-400]="vm.page === 1"
|
||||||
|
class="text-xl border rounded-md p-3"
|
||||||
|
(click)="store.previousPage()">
|
||||||
|
<
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
[disabled]="vm.endOfPage"
|
||||||
|
[class.bg-gray-400]="vm.endOfPage"
|
||||||
|
class="text-xl border rounded-md p-3"
|
||||||
|
(click)="store.nextPage()">
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
Page :{{ vm.page }} / {{ vm.pages }}
|
||||||
|
</section>
|
||||||
|
<ul
|
||||||
|
class="flex flex-wrap gap-4"
|
||||||
|
*ngIf="vm.photos && vm.photos.length > 0; else noPhoto">
|
||||||
|
<li *ngFor="let photo of vm.photos; trackBy: trackById">
|
||||||
|
<a routerLink="detail" [queryParams]="{ photo: encode(photo) }">
|
||||||
|
<img
|
||||||
|
src="{{ photo.url_q }}"
|
||||||
|
alt="{{ photo.title }}"
|
||||||
|
class="image" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ng-template #noPhoto>
|
||||||
|
<div>No Photos found. Type a search word.</div>
|
||||||
|
</ng-template>
|
||||||
|
<footer class="text-red-500">
|
||||||
|
{{ vm.error }}
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
</ng-container>
|
||||||
|
`,
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
118
apps/rxjs-to-signal/src/app/list/photos.store.ts
Normal file
118
apps/rxjs-to-signal/src/app/list/photos.store.ts
Normal file
@@ -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<PhotoState>
|
||||||
|
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 })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
10
apps/rxjs-to-signal/src/app/photo.model.ts
Normal file
10
apps/rxjs-to-signal/src/app/photo.model.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
39
apps/rxjs-to-signal/src/app/photos.service.ts
Normal file
39
apps/rxjs-to-signal/src/app/photos.service.ts
Normal file
@@ -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<FlickrAPIResponse> {
|
||||||
|
return this.http.get<FlickrAPIResponse>(
|
||||||
|
'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',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
apps/rxjs-to-signal/src/assets/.gitkeep
Normal file
0
apps/rxjs-to-signal/src/assets/.gitkeep
Normal file
BIN
apps/rxjs-to-signal/src/favicon.ico
Normal file
BIN
apps/rxjs-to-signal/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
13
apps/rxjs-to-signal/src/index.html
Normal file
13
apps/rxjs-to-signal/src/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>rxjs-to-signal</title>
|
||||||
|
<base href="/" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
7
apps/rxjs-to-signal/src/main.ts
Normal file
7
apps/rxjs-to-signal/src/main.ts
Normal file
@@ -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)
|
||||||
|
);
|
||||||
5
apps/rxjs-to-signal/src/styles.scss
Normal file
5
apps/rxjs-to-signal/src/styles.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* You can add global styles to this file, and also import other style files */
|
||||||
2
apps/rxjs-to-signal/src/test-setup.ts
Normal file
2
apps/rxjs-to-signal/src/test-setup.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import 'jest-preset-angular/setup-jest';
|
||||||
14
apps/rxjs-to-signal/tailwind.config.js
Normal file
14
apps/rxjs-to-signal/tailwind.config.js
Normal file
@@ -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: [],
|
||||||
|
};
|
||||||
10
apps/rxjs-to-signal/tsconfig.app.json
Normal file
10
apps/rxjs-to-signal/tsconfig.app.json
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
7
apps/rxjs-to-signal/tsconfig.editor.json
Normal file
7
apps/rxjs-to-signal/tsconfig.editor.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["jest", "node"]
|
||||||
|
}
|
||||||
|
}
|
||||||
32
apps/rxjs-to-signal/tsconfig.json
Normal file
32
apps/rxjs-to-signal/tsconfig.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
15
apps/rxjs-to-signal/tsconfig.spec.json
Normal file
15
apps/rxjs-to-signal/tsconfig.spec.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user