├── .editorconfig
├── .gitignore
├── .vscode
├── extensions.json
├── launch.json
└── tasks.json
├── README.md
├── Untitled
├── angular.json
├── bun.lockb
├── package.json
├── public
└── favicon.ico
├── server.ts
├── src
├── app
│ ├── adapters
│ │ ├── character.adapter.ts
│ │ └── index.ts
│ ├── app.component.html
│ ├── app.component.scss
│ ├── app.component.ts
│ ├── app.config.server.ts
│ ├── app.config.ts
│ ├── app.routes.ts
│ ├── components
│ │ ├── custom-input
│ │ │ ├── custom-input.component.html
│ │ │ ├── custom-input.component.scss
│ │ │ └── custom-input.component.ts
│ │ └── index.ts
│ ├── main-container
│ │ ├── components
│ │ │ ├── character-add-edit
│ │ │ │ ├── character-add-edit.component.html
│ │ │ │ ├── character-add-edit.component.scss
│ │ │ │ └── character-add-edit.component.ts
│ │ │ ├── character-card
│ │ │ │ ├── character-card.component.html
│ │ │ │ ├── character-card.component.scss
│ │ │ │ └── character-card.component.ts
│ │ │ └── index.ts
│ │ ├── main-container.component.html
│ │ ├── main-container.component.scss
│ │ └── main-container.component.ts
│ ├── models
│ │ ├── character.model.ts
│ │ └── index.ts
│ ├── services
│ │ ├── character.service.ts
│ │ └── index.ts
│ └── store
│ │ ├── global.store.ts
│ │ └── index.ts
├── index.html
├── main.server.ts
├── main.ts
└── styles.scss
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json
/.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 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
3 | "recommendations": ["angular.ng-template"]
4 | }
5 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # InterviewAngular
2 |
3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.1.0.
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 |
--------------------------------------------------------------------------------
/Untitled:
--------------------------------------------------------------------------------
1 | 7f1d37251499db2a837c212f6016c0d6b16c8e97 initial commit
2 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "interview-angular": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "scss",
11 | "changeDetection": "OnPush",
12 | "skipTests": true
13 | },
14 | "@schematics/angular:service": {
15 | "skipTests": true
16 | }
17 | },
18 | "root": "",
19 | "sourceRoot": "src",
20 | "prefix": "app",
21 | "architect": {
22 | "build": {
23 | "builder": "@angular-devkit/build-angular:application",
24 | "options": {
25 | "outputPath": "dist/interview-angular",
26 | "index": "src/index.html",
27 | "browser": "src/main.ts",
28 | "polyfills": [],
29 | "tsConfig": "tsconfig.app.json",
30 | "inlineStyleLanguage": "scss",
31 | "assets": [
32 | {
33 | "glob": "**/*",
34 | "input": "public"
35 | }
36 | ],
37 | "styles": [
38 | "src/styles.scss"
39 | ],
40 | "scripts": [],
41 | "server": "src/main.server.ts",
42 | "prerender": true,
43 | "ssr": {
44 | "entry": "server.ts"
45 | }
46 | },
47 | "configurations": {
48 | "production": {
49 | "budgets": [
50 | {
51 | "type": "initial",
52 | "maximumWarning": "500kB",
53 | "maximumError": "1MB"
54 | },
55 | {
56 | "type": "anyComponentStyle",
57 | "maximumWarning": "2kB",
58 | "maximumError": "4kB"
59 | }
60 | ],
61 | "outputHashing": "all"
62 | },
63 | "development": {
64 | "optimization": false,
65 | "extractLicenses": false,
66 | "sourceMap": true
67 | }
68 | },
69 | "defaultConfiguration": "production"
70 | },
71 | "serve": {
72 | "builder": "@angular-devkit/build-angular:dev-server",
73 | "configurations": {
74 | "production": {
75 | "buildTarget": "interview-angular:build:production"
76 | },
77 | "development": {
78 | "buildTarget": "interview-angular:build:development"
79 | }
80 | },
81 | "defaultConfiguration": "development"
82 | },
83 | "extract-i18n": {
84 | "builder": "@angular-devkit/build-angular:extract-i18n"
85 | },
86 | "test": {
87 | "builder": "@angular-devkit/build-angular:karma",
88 | "options": {
89 | "polyfills": [],
90 | "tsConfig": "tsconfig.spec.json",
91 | "inlineStyleLanguage": "scss",
92 | "assets": [
93 | {
94 | "glob": "**/*",
95 | "input": "public"
96 | }
97 | ],
98 | "styles": [
99 | "src/styles.scss"
100 | ],
101 | "scripts": []
102 | }
103 | }
104 | }
105 | }
106 | },
107 | "cli": {
108 | "analytics": "3a299bc9-59fd-4c91-9719-b38e83853884"
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gentleman-Programming/Angular-18-interview/e40424013e53313648b242082d8a19fd84e18846/bun.lockb
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "interview-angular",
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 | "serve:ssr:interview-angular": "node dist/interview-angular/server/server.mjs"
11 | },
12 | "private": true,
13 | "dependencies": {
14 | "@angular/animations": "^18.1.0",
15 | "@angular/common": "^18.1.0",
16 | "@angular/compiler": "^18.1.0",
17 | "@angular/core": "^18.1.0",
18 | "@angular/forms": "^18.1.0",
19 | "@angular/platform-browser": "^18.1.0",
20 | "@angular/platform-browser-dynamic": "^18.1.0",
21 | "@angular/platform-server": "^18.1.0",
22 | "@angular/router": "^18.1.0",
23 | "@angular/ssr": "^18.1.0",
24 | "@ngrx/signals": "^18.0.2",
25 | "express": "^4.18.2",
26 | "rxjs": "~7.8.0",
27 | "tslib": "^2.3.0",
28 | "zone.js": "~0.14.3"
29 | },
30 | "devDependencies": {
31 | "@angular-devkit/build-angular": "^18.1.0",
32 | "@angular/cli": "^18.1.0",
33 | "@angular/compiler-cli": "^18.1.0",
34 | "@types/express": "^4.17.17",
35 | "@types/jasmine": "~5.1.0",
36 | "@types/node": "^18.18.0",
37 | "jasmine-core": "~5.1.0",
38 | "karma": "~6.4.0",
39 | "karma-chrome-launcher": "~3.2.0",
40 | "karma-coverage": "~2.2.0",
41 | "karma-jasmine": "~5.1.0",
42 | "karma-jasmine-html-reporter": "~2.1.0",
43 | "typescript": "~5.5.2"
44 | }
45 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gentleman-Programming/Angular-18-interview/e40424013e53313648b242082d8a19fd84e18846/public/favicon.ico
--------------------------------------------------------------------------------
/server.ts:
--------------------------------------------------------------------------------
1 | import { APP_BASE_HREF } from '@angular/common';
2 | import { CommonEngine } from '@angular/ssr';
3 | import express from 'express';
4 | import { fileURLToPath } from 'node:url';
5 | import { dirname, join, resolve } from 'node:path';
6 | import bootstrap from './src/main.server';
7 |
8 | // The Express app is exported so that it can be used by serverless Functions.
9 | export function app(): express.Express {
10 | const server = express();
11 | const serverDistFolder = dirname(fileURLToPath(import.meta.url));
12 | const browserDistFolder = resolve(serverDistFolder, '../browser');
13 | const indexHtml = join(serverDistFolder, 'index.server.html');
14 |
15 | const commonEngine = new CommonEngine();
16 |
17 | server.set('view engine', 'html');
18 | server.set('views', browserDistFolder);
19 |
20 | // Example Express Rest API endpoints
21 | // server.get('/api/**', (req, res) => { });
22 | // Serve static files from /browser
23 | server.get('**', express.static(browserDistFolder, {
24 | maxAge: '1y',
25 | index: 'index.html',
26 | }));
27 |
28 | // All regular routes use the Angular engine
29 | server.get('**', (req, res, next) => {
30 | const { protocol, originalUrl, baseUrl, headers } = req;
31 |
32 | commonEngine
33 | .render({
34 | bootstrap,
35 | documentFilePath: indexHtml,
36 | url: `${protocol}://${headers.host}${originalUrl}`,
37 | publicPath: browserDistFolder,
38 | providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
39 | })
40 | .then((html) => res.send(html))
41 | .catch((err) => next(err));
42 | });
43 |
44 | return server;
45 | }
46 |
47 | function run(): void {
48 | const port = process.env['PORT'] || 4000;
49 |
50 | // Start up the Node server
51 | const server = app();
52 | server.listen(port, () => {
53 | console.log(`Node Express server listening on http://localhost:${port}`);
54 | });
55 | }
56 |
57 | run();
58 |
--------------------------------------------------------------------------------
/src/app/adapters/character.adapter.ts:
--------------------------------------------------------------------------------
1 | import { Character, CharacterInfo } from "@app/models";
2 |
3 | export const CharacterAdapter = (characterInfo: CharacterInfo): Character[] => ([...characterInfo.results])
4 |
--------------------------------------------------------------------------------
/src/app/adapters/index.ts:
--------------------------------------------------------------------------------
1 | export * from './character.adapter'
2 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gentleman-Programming/Angular-18-interview/e40424013e53313648b242082d8a19fd84e18846/src/app/app.component.scss
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component } from '@angular/core';
2 | import { RouterOutlet } from '@angular/router';
3 |
4 | @Component({
5 | selector: 'app-root',
6 | standalone: true,
7 | imports: [RouterOutlet],
8 | templateUrl: './app.component.html',
9 | styleUrl: './app.component.scss',
10 | changeDetection: ChangeDetectionStrategy.OnPush,
11 | })
12 | export class AppComponent { }
13 |
--------------------------------------------------------------------------------
/src/app/app.config.server.ts:
--------------------------------------------------------------------------------
1 | import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
2 | import { provideServerRendering } from '@angular/platform-server';
3 | import { appConfig } from './app.config';
4 |
5 | const serverConfig: ApplicationConfig = {
6 | providers: [
7 | provideServerRendering()
8 | ]
9 | };
10 |
11 | export const config = mergeApplicationConfig(appConfig, serverConfig);
12 |
--------------------------------------------------------------------------------
/src/app/app.config.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ApplicationConfig,
3 | provideExperimentalZonelessChangeDetection,
4 | } from '@angular/core';
5 | import { provideRouter, withComponentInputBinding } from '@angular/router';
6 |
7 | import { routes } from './app.routes';
8 | import { provideClientHydration } from '@angular/platform-browser';
9 | import { provideHttpClient, withFetch } from '@angular/common/http';
10 |
11 | export const appConfig: ApplicationConfig = {
12 | providers: [
13 | provideRouter(routes, withComponentInputBinding()),
14 | provideClientHydration(),
15 | provideExperimentalZonelessChangeDetection(),
16 | provideHttpClient(withFetch()),
17 | ],
18 | };
19 |
--------------------------------------------------------------------------------
/src/app/app.routes.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@angular/router';
2 |
3 | export const routes: Routes = [
4 | {
5 | path: '',
6 | redirectTo: '/characters',
7 | pathMatch: 'full',
8 | },
9 | {
10 | path: 'characters',
11 | loadComponent: () =>
12 | import('./main-container/main-container.component').then(
13 | (m) => m.MainContainerComponent,
14 | ),
15 | },
16 | {
17 | path: 'add-edit-character',
18 | loadComponent: () =>
19 | import(
20 | './main-container/components/character-add-edit/character-add-edit.component'
21 | ).then((m) => m.CharacterAddEditComponent),
22 | },
23 | {
24 | path: 'add-edit-character/:id',
25 | loadComponent: () =>
26 | import(
27 | './main-container/components/character-add-edit/character-add-edit.component'
28 | ).then((m) => m.CharacterAddEditComponent),
29 | },
30 | ];
31 |
--------------------------------------------------------------------------------
/src/app/components/custom-input/custom-input.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 | @if (control().invalid && control().touched) {
11 | {{ errorMessage() }}
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/app/components/custom-input/custom-input.component.scss:
--------------------------------------------------------------------------------
1 | .form-group {
2 | margin-bottom: 15px;
3 |
4 | label {
5 | display: block;
6 | margin-bottom: 5px;
7 | font-weight: bold;
8 | }
9 |
10 | input {
11 | width: 100%;
12 | padding: 10px;
13 | border: 1px solid #ddd;
14 | border-radius: 4px;
15 | }
16 |
17 | .error {
18 | color: red;
19 |
20 | font-size: 0.875rem;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/components/custom-input/custom-input.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, input } from '@angular/core';
2 | import { FormControl, ReactiveFormsModule } from '@angular/forms';
3 |
4 | @Component({
5 | selector: 'app-custom-input',
6 | standalone: true,
7 | imports: [ReactiveFormsModule],
8 | templateUrl: './custom-input.component.html',
9 | styleUrl: './custom-input.component.scss',
10 | changeDetection: ChangeDetectionStrategy.OnPush,
11 | })
12 | export class CustomInputComponent {
13 | control = input.required();
14 | label = input.required();
15 | type = input.required();
16 | placeHolder = input.required();
17 | errorMessage = input.required();
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './custom-input/custom-input.component';
2 |
--------------------------------------------------------------------------------
/src/app/main-container/components/character-add-edit/character-add-edit.component.html:
--------------------------------------------------------------------------------
1 |
24 |
--------------------------------------------------------------------------------
/src/app/main-container/components/character-add-edit/character-add-edit.component.scss:
--------------------------------------------------------------------------------
1 | .character-add-container {
2 | max-width: 600px;
3 | margin: 0 auto;
4 | padding: 20px;
5 | background-color: #f9f9f9;
6 |
7 | border-radius: 8px;
8 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/main-container/components/character-add-edit/character-add-edit.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | computed,
5 | inject,
6 | input,
7 | Signal,
8 | } from '@angular/core';
9 | import { GlobalStore } from '@app/store';
10 | import {
11 | FormControl,
12 | FormGroup,
13 | ReactiveFormsModule,
14 | Validators,
15 | } from '@angular/forms';
16 | import { emptyCharacter } from '@app/models';
17 | import { CustomInputComponent } from '@app/components';
18 |
19 | interface CharacterForm {
20 | name: FormControl;
21 | image: FormControl;
22 | }
23 |
24 | @Component({
25 | selector: 'app-character-add-edit',
26 | standalone: true,
27 | imports: [ReactiveFormsModule, CustomInputComponent],
28 | templateUrl: './character-add-edit.component.html',
29 | styleUrl: './character-add-edit.component.scss',
30 | changeDetection: ChangeDetectionStrategy.OnPush,
31 | })
32 | export class CharacterAddEditComponent {
33 | id = input();
34 |
35 | readonly store = inject(GlobalStore);
36 |
37 | characterToEdit = computed(
38 | () => this.store.getCharacter(Number(this.id())) ?? emptyCharacter,
39 | );
40 |
41 | characterForm: Signal = computed(
42 | () =>
43 | new FormGroup({
44 | name: new FormControl(this.characterToEdit().name, {
45 | nonNullable: true,
46 | validators: [Validators.required],
47 | }),
48 | image: new FormControl(this.characterToEdit().image, {
49 | nonNullable: true,
50 | validators: [Validators.required],
51 | }),
52 | }),
53 | );
54 |
55 | onSubmit(): void {
56 | if (this.characterForm().valid) {
57 | const character = {
58 | ...(this.id() ? { id: Number(this.id()) } : {}),
59 | ...this.characterForm().value,
60 | };
61 |
62 | const methodToUse = this.id() ? 'updateCharacter' : 'addCharacter';
63 |
64 | this.store[methodToUse](character);
65 |
66 | this.characterForm().reset();
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/app/main-container/components/character-card/character-card.component.html:
--------------------------------------------------------------------------------
1 | @let characterLocal = character();
2 |
3 | @if (characterLocal) {
4 |
5 |
{{ characterLocal.name }}
6 |
7 |
![]()
14 |
15 |
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/main-container/components/character-card/character-card.component.scss:
--------------------------------------------------------------------------------
1 | .character-card {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | text-align: center;
6 | gap: 1rem;
7 | }
8 |
9 | .character-image {
10 | width: 100px;
11 | height: 100px;
12 | object-fit: cover;
13 | border-radius: 50%;
14 | margin-bottom: 10px;
15 | }
16 |
17 | .button-container {
18 | display: flex;
19 | flex-direction: line;
20 | align-items: center;
21 | gap: 1rem;
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/main-container/components/character-card/character-card.component.ts:
--------------------------------------------------------------------------------
1 | import { NgOptimizedImage } from '@angular/common';
2 | import {
3 | ChangeDetectionStrategy,
4 | Component,
5 | inject,
6 | input,
7 | } from '@angular/core';
8 | import { RouterLink } from '@angular/router';
9 | import { Character } from '@app/models';
10 | import { GlobalStore } from '@app/store';
11 |
12 | @Component({
13 | selector: 'app-character-card',
14 | standalone: true,
15 | imports: [NgOptimizedImage, RouterLink],
16 | templateUrl: './character-card.component.html',
17 | styleUrl: './character-card.component.scss',
18 | changeDetection: ChangeDetectionStrategy.OnPush,
19 | })
20 | export class CharacterCardComponent {
21 | character = input.required();
22 |
23 | readonly store = inject(GlobalStore);
24 |
25 | removeCharacter(id: number) {
26 | this.store.removeCharacter(id);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/main-container/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './character-card/character-card.component';
2 | export * from './character-add-edit/character-add-edit.component';
3 |
--------------------------------------------------------------------------------
/src/app/main-container/main-container.component.html:
--------------------------------------------------------------------------------
1 | Add Character
2 |
3 | @defer (when store.characters().length) {
4 | @for (character of store.characters(); track character.id) {
5 | @defer (on viewport) {
6 |
7 | } @placeholder {
8 |
Loading
9 | }
10 | }
11 | } @placeholder {
12 |
Loading
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/src/app/main-container/main-container.component.scss:
--------------------------------------------------------------------------------
1 | .character-grid {
2 | display: grid;
3 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
4 | gap: 20px;
5 | padding: 20px;
6 | }
7 |
8 | .character-card {
9 | background-color: #f9f9f9;
10 | border: 1px solid #ddd;
11 | border-radius: 8px;
12 | padding: 16px;
13 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
14 | }
15 |
16 | .character-card h3 {
17 | font-size: 1.5rem;
18 | margin-top: 0;
19 | }
20 |
21 | .character-card h2 {
22 | font-size: 1.25rem;
23 | margin-bottom: 0;
24 | }
25 |
26 | .character-image {
27 | width: 100%;
28 | height: auto;
29 | border-radius: 4px;
30 |
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/app/main-container/main-container.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
2 | import { GlobalStore } from '@app/store';
3 | import { CharacterCardComponent } from './components';
4 | import { RouterLink } from '@angular/router';
5 |
6 | @Component({
7 | selector: 'app-main-container',
8 | standalone: true,
9 | imports: [CharacterCardComponent, RouterLink],
10 | templateUrl: './main-container.component.html',
11 | styleUrl: './main-container.component.scss',
12 | changeDetection: ChangeDetectionStrategy.OnPush,
13 | })
14 | export class MainContainerComponent {
15 | readonly store = inject(GlobalStore);
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/models/character.model.ts:
--------------------------------------------------------------------------------
1 | export interface Info {
2 | count: number;
3 | pages: number;
4 | next: string;
5 | prev: string;
6 | }
7 |
8 | export enum Gender {
9 | 'MALE' = 'Male',
10 | 'FEMALE' = 'Female',
11 | 'GENDERLESS' = 'Genderless',
12 | 'UNKNOWN' = 'unknown',
13 | }
14 |
15 | export interface CharacterInfo {
16 | info: Info;
17 | results: Character[];
18 | }
19 |
20 | export interface LinkedElement {
21 | name: string;
22 | link: string;
23 | }
24 |
25 | export interface Origin extends LinkedElement { }
26 | export interface Location extends LinkedElement { }
27 |
28 |
29 | export interface Character {
30 | id: number;
31 | name: string;
32 | status: string;
33 | species: string;
34 | type: string;
35 | gender: Gender;
36 | origin: Origin;
37 | location: Location;
38 | image: string;
39 | episode: string[];
40 | url: string;
41 | created: string;
42 | }
43 |
44 | export const emptyCharacter: Character = {
45 | id: 0,
46 | name: '',
47 | status: '',
48 | species: '',
49 | type: '',
50 | gender: Gender.MALE, // Puedes usar un valor predeterminado adecuado para el tipo Gender
51 | origin: {
52 | name: '',
53 | link: '',
54 | },
55 | location: {
56 | name: '',
57 | link: '',
58 | },
59 | image: '',
60 | episode: [],
61 | url: '',
62 | created: '',
63 | };
64 |
--------------------------------------------------------------------------------
/src/app/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from './character.model'
2 |
--------------------------------------------------------------------------------
/src/app/services/character.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from '@angular/common/http';
2 | import { inject, Injectable } from '@angular/core';
3 | import { CharacterAdapter } from '@app/adapters';
4 | import { Character, CharacterInfo } from '@app/models';
5 | import { catchError, map, Observable } from 'rxjs';
6 |
7 | @Injectable({
8 | providedIn: 'root'
9 | })
10 | export class CharacterService {
11 | private readonly baseUrl = 'https://rickandmortyapi.com/api/character';
12 | http = inject(HttpClient)
13 |
14 | getAllCharacters(): Observable {
15 | return this.http.get(this.baseUrl).pipe(map(info => CharacterAdapter(info)))
16 | }
17 |
18 | addCharacter(character: Omit): Observable {
19 | return this.http.post(this.baseUrl, { character }).pipe(
20 | catchError(() => {
21 | console.info("error prevented for testing")
22 | return Promise.resolve()
23 | })
24 | )
25 | }
26 |
27 | removeCharacter(id: number): Observable {
28 | const url = `${this.baseUrl}/${id}`
29 | return this.http.delete(url).pipe(
30 | catchError(() => {
31 | console.info("error prevented for testing")
32 | return Promise.resolve()
33 | })
34 | )
35 | }
36 |
37 | updateCharacter(character: Character): Observable {
38 | return this.http.put(this.baseUrl, { character }).pipe(
39 | catchError(() => {
40 | console.info("error prevented for testing")
41 | return Promise.resolve()
42 | })
43 | )
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './character.service'
2 |
--------------------------------------------------------------------------------
/src/app/store/global.store.ts:
--------------------------------------------------------------------------------
1 | import { inject, InjectionToken } from '@angular/core';
2 | import { Character } from '@app/models';
3 | import { CharacterService } from '@app/services';
4 | import {
5 | patchState,
6 | signalStore,
7 | withHooks,
8 | withMethods,
9 | withState,
10 | } from '@ngrx/signals';
11 | import { withEntities } from '@ngrx/signals/entities';
12 | import { lastValueFrom } from 'rxjs';
13 |
14 | type StoreState = {
15 | characters: Character[];
16 | };
17 |
18 | const initialState: StoreState = {
19 | characters: [],
20 | };
21 |
22 | const STORE_STATE = new InjectionToken('GlobalStore', {
23 | factory: () => initialState,
24 | });
25 |
26 | export const GlobalStore = signalStore(
27 | { providedIn: 'root' },
28 | withState(() => inject(STORE_STATE)),
29 | withEntities(),
30 | withMethods((store, characterService = inject(CharacterService)) => ({
31 | getCharacter(id: number) {
32 | return store.characters().find((char) => char.id === id);
33 | },
34 |
35 | async addCharacter(character: Omit) {
36 | try {
37 | await lastValueFrom(characterService.addCharacter(character));
38 |
39 | patchState(store, ({ characters }) => ({
40 | characters: [
41 | ...characters,
42 | { id: new Date().getTime(), ...character },
43 | ],
44 | }));
45 | } catch (error) {}
46 | },
47 |
48 | async removeCharacter(id: number) {
49 | try {
50 | await lastValueFrom(characterService.removeCharacter(id));
51 |
52 | patchState(store, ({ characters }) => ({
53 | characters: characters.filter((char) => char.id !== id),
54 | }));
55 | } catch (error) {}
56 | },
57 |
58 | async updateCharacter(character: Character) {
59 | try {
60 | await lastValueFrom(characterService.updateCharacter(character));
61 |
62 | patchState(store, ({ characters }) => ({
63 | characters: characters.map((char) =>
64 | char.id === character.id ? { ...char, ...character } : char,
65 | ),
66 | isLoading: false,
67 | }));
68 | } catch (error) {}
69 | },
70 | })),
71 | withHooks({
72 | async onInit(store, characterService = inject(CharacterService)) {
73 | const characters = await lastValueFrom(
74 | characterService.getAllCharacters(),
75 | );
76 |
77 | patchState(store, { characters });
78 | },
79 | }),
80 | );
81 |
--------------------------------------------------------------------------------
/src/app/store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './global.store'
2 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | InterviewAngular
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/main.server.ts:
--------------------------------------------------------------------------------
1 | import { bootstrapApplication } from '@angular/platform-browser';
2 | import { AppComponent } from './app/app.component';
3 | import { config } from './app/app.config.server';
4 |
5 | const bootstrap = () => bootstrapApplication(AppComponent, config);
6 |
7 | export default bootstrap;
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/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 |
8 | "types": ["node"]
9 | },
10 | "files": ["src/main.ts", "src/main.server.ts", "server.ts"],
11 | "include": ["src/**/*.d.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/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 | "esModuleInterop": true,
14 | "sourceMap": true,
15 | "declaration": false,
16 | "experimentalDecorators": true,
17 | "moduleResolution": "bundler",
18 | "importHelpers": true,
19 | "target": "ES2022",
20 | "module": "ES2022",
21 | "lib": [
22 | "ES2022",
23 | "dom"
24 | ],
25 | "baseUrl": "./",
26 | "paths": {
27 | "@app/*": [
28 | "src/app/*"
29 | ]
30 | }
31 | },
32 | "angularCompilerOptions": {
33 | "enableI18nLegacyMessageIdFormat": false,
34 | "strictInjectionParameters": true,
35 | "strictInputAccessModifiers": true,
36 | "strictTemplates": true
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------