First commit with existing project files
42
.gitignore
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# Node
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
27
README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# PlayingCards
|
||||||
|
|
||||||
|
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.2.20.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Further help
|
||||||
|
|
||||||
|
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.
|
||||||
98
angular.json
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"playing-cards": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:application",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/playing-cards",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||||
|
"src/styles.css"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kB",
|
||||||
|
"maximumError": "1MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "2kB",
|
||||||
|
"maximumError": "4kB"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "playing-cards:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "playing-cards:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js",
|
||||||
|
"zone.js/testing"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||||
|
"src/styles.css"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14158
package-lock.json
generated
Normal file
40
package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "playing-cards",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "^18.2.0",
|
||||||
|
"@angular/cdk": "^18.2.14",
|
||||||
|
"@angular/common": "^18.2.0",
|
||||||
|
"@angular/compiler": "^18.2.0",
|
||||||
|
"@angular/core": "^18.2.0",
|
||||||
|
"@angular/forms": "^18.2.0",
|
||||||
|
"@angular/material": "^18.2.14",
|
||||||
|
"@angular/platform-browser": "^18.2.0",
|
||||||
|
"@angular/platform-browser-dynamic": "^18.2.0",
|
||||||
|
"@angular/router": "^18.2.0",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.14.10"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^18.2.20",
|
||||||
|
"@angular/cli": "^18.2.20",
|
||||||
|
"@angular/compiler-cli": "^18.2.0",
|
||||||
|
"@types/jasmine": "~5.1.0",
|
||||||
|
"jasmine-core": "~5.2.0",
|
||||||
|
"karma": "~6.4.0",
|
||||||
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
"karma-coverage": "~2.2.0",
|
||||||
|
"karma-jasmine": "~5.1.0",
|
||||||
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"typescript": "~5.5.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/assets/img/bulb.webp
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
public/assets/img/car.webp
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
public/assets/img/char.webp
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
public/assets/img/electric.png
Normal file
|
After Width: | Height: | Size: 457 KiB |
BIN
public/assets/img/fire.png
Normal file
|
After Width: | Height: | Size: 416 KiB |
BIN
public/assets/img/pik.webp
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
public/assets/img/plant.png
Normal file
|
After Width: | Height: | Size: 295 KiB |
BIN
public/assets/img/search.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/assets/img/water.png
Normal file
|
After Width: | Height: | Size: 287 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
10
src/app/app.component.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
mat-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/app/app.component.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
@if (loginService.user()) {
|
||||||
|
<mat-toolbar>
|
||||||
|
<button mat-icon-button (click)="navigateHome()">
|
||||||
|
<mat-icon>home</mat-icon>
|
||||||
|
</button>
|
||||||
|
@if (loginService.user()?.firstName && loginService.user()?.lastName) {
|
||||||
|
<p class="mat-title">Welcome, {{ loginService.user()?.firstName }} {{ loginService.user()?.lastName }}</p>
|
||||||
|
} @else {
|
||||||
|
<p>Welcome, {{ loginService.user()?.username }}</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<button mat-flat-button color="warn" (click)="logout()">Logout</button>
|
||||||
|
</mat-toolbar>
|
||||||
|
}
|
||||||
|
<router-outlet></router-outlet>
|
||||||
29
src/app/app.component.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
describe('AppComponent', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AppComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should have the 'playing-cards' title`, () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app.title).toEqual('playing-cards');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render title', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, playing-cards');
|
||||||
|
});
|
||||||
|
});
|
||||||
51
src/app/app.component.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {Component, inject, OnDestroy} from '@angular/core';
|
||||||
|
import {Router, RouterOutlet} from '@angular/router';
|
||||||
|
import {MatToolbar} from '@angular/material/toolbar';
|
||||||
|
import {MatButton, MatIconButton} from '@angular/material/button';
|
||||||
|
import {MatIcon} from '@angular/material/icon';
|
||||||
|
import {LoginService} from './services/login/login.service';
|
||||||
|
import {Subscription} from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
RouterOutlet,
|
||||||
|
MatToolbar,
|
||||||
|
MatIconButton,
|
||||||
|
MatIcon,
|
||||||
|
MatButton
|
||||||
|
],
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrl: './app.component.css',
|
||||||
|
})
|
||||||
|
export class AppComponent implements OnDestroy {
|
||||||
|
|
||||||
|
private readonly router: Router = inject(Router);
|
||||||
|
protected readonly loginService: LoginService = inject(LoginService);
|
||||||
|
|
||||||
|
private logoutSubscription: Subscription | null = null;
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.logoutSubscription?.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.logoutSubscription = this.loginService.logout().subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.navigateToLogin();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.navigateToLogin()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToLogin() {
|
||||||
|
this.router.navigate(['/login']).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateHome() {
|
||||||
|
this.router.navigate(['/home']).then();
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/app/app.config.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
|
||||||
|
import {provideRouter} from '@angular/router';
|
||||||
|
|
||||||
|
import {routes} from './app.routes';
|
||||||
|
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
|
||||||
|
import {provideHttpClient, withInterceptors} from '@angular/common/http';
|
||||||
|
import {authTokenInterceptor} from './interceptors/auth-token.interceptor';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideZoneChangeDetection({eventCoalescing: true}),
|
||||||
|
provideRouter(routes),
|
||||||
|
provideAnimationsAsync(),
|
||||||
|
provideHttpClient(withInterceptors([
|
||||||
|
authTokenInterceptor
|
||||||
|
])
|
||||||
|
)
|
||||||
|
]
|
||||||
|
};
|
||||||
42
src/app/app.routes.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import {Routes} from '@angular/router';
|
||||||
|
import {MonsterListComponent} from './pages/monster-list/monster-list.component';
|
||||||
|
import {MonsterComponent} from './pages/monster/monster.component';
|
||||||
|
import {NotFoundComponent} from './pages/not-found/not-found.component';
|
||||||
|
import {LoginComponent} from './pages/login/login.component';
|
||||||
|
import {isLoggedInGuard} from './guards/is-logged-in.guard';
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: 'home',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'home',
|
||||||
|
component: MonsterListComponent,
|
||||||
|
//canActivate: [isLoggedInGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'login',
|
||||||
|
component: LoginComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'monster',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: MonsterComponent,
|
||||||
|
//canActivate: [isLoggedInGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
component: MonsterComponent,
|
||||||
|
//canActivate: [isLoggedInGuard]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '**',
|
||||||
|
component: NotFoundComponent
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<h2 mat-dialog-title>Delete Monster</h2>
|
||||||
|
<mat-dialog-content>
|
||||||
|
<p>Do you want to delete the selected monster ?</p>
|
||||||
|
</mat-dialog-content>
|
||||||
|
<mat-dialog-actions>
|
||||||
|
<button mat-button [mat-dialog-close]="false">No</button>
|
||||||
|
<button mat-button [mat-dialog-close]="true">Yes</button>
|
||||||
|
</mat-dialog-actions>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DeleteMonsterConfirmationDialogComponent } from './delete-monster-confirmation-dialog.component';
|
||||||
|
|
||||||
|
describe('DeleteMonsterConfirmationDialogComponent', () => {
|
||||||
|
let component: DeleteMonsterConfirmationDialogComponent;
|
||||||
|
let fixture: ComponentFixture<DeleteMonsterConfirmationDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [DeleteMonsterConfirmationDialogComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DeleteMonsterConfirmationDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import {MatDialogActions, MatDialogClose, MatDialogContent, MatDialogTitle} from '@angular/material/dialog';
|
||||||
|
import {MatButton} from '@angular/material/button';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-delete-monster-confirmation-dialog',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
MatDialogTitle,
|
||||||
|
MatDialogActions,
|
||||||
|
MatDialogContent,
|
||||||
|
MatButton,
|
||||||
|
MatDialogClose
|
||||||
|
],
|
||||||
|
templateUrl: './delete-monster-confirmation-dialog.component.html',
|
||||||
|
styleUrl: './delete-monster-confirmation-dialog.component.css'
|
||||||
|
})
|
||||||
|
export class DeleteMonsterConfirmationDialogComponent {
|
||||||
|
|
||||||
|
}
|
||||||
97
src/app/components/playing-card/playing-card.component.css
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#card {
|
||||||
|
display: block;
|
||||||
|
width: 250px;
|
||||||
|
height: 350px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 5px 5px 10px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
background-color: rgb(221, 221, 221);
|
||||||
|
}
|
||||||
|
|
||||||
|
#inside {
|
||||||
|
padding: 5px;
|
||||||
|
background-color: rgb(255, 247, 97);
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.left,
|
||||||
|
.right {
|
||||||
|
display: flex;
|
||||||
|
width: 50%;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right #hp,
|
||||||
|
.right img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon.energy {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure#art {
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px 5px 0 5px;
|
||||||
|
width: calc(100% - 10px);
|
||||||
|
background-color: lightgray;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 175px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
text-align: center;
|
||||||
|
font-size: smaller;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#capacities {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capacity {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 10px 0;
|
||||||
|
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: smaller
|
||||||
|
}
|
||||||
32
src/app/components/playing-card/playing-card.component.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<div id="card">
|
||||||
|
<div id="inside" [style.background-color]="backgroundColor()">
|
||||||
|
<header>
|
||||||
|
<div class="left">
|
||||||
|
<div id="name">{{ monster().name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<div id="hp">{{ monster().hp }}</div>
|
||||||
|
<img class="energy icon" [src]="MonsterTypeIcon()" alt="Monster Type Icon">
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<figure id="art">
|
||||||
|
<img [src]="monster().image" alt="{{ monster().name }}" />
|
||||||
|
<figcaption>{{ monster().figureCaption }}</figcaption>
|
||||||
|
</figure>
|
||||||
|
<div id="capacities">
|
||||||
|
<div class="capacity">
|
||||||
|
<div class="main">
|
||||||
|
<div class="cost">
|
||||||
|
<img class="energy icon" src="assets/img/electric.png" alt="Energy Icon">
|
||||||
|
<img class="energy icon" src="assets/img/electric.png" alt="Energy Icon">
|
||||||
|
</div>
|
||||||
|
<div class="name">{{ monster().attackName }}</div>
|
||||||
|
<div class="damage">{{ monster().attackStrength }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
{{ monster().attackDescription }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PlayingCardComponent } from './playing-card.component';
|
||||||
|
|
||||||
|
describe('PlayingCardComponent', () => {
|
||||||
|
let component: PlayingCardComponent;
|
||||||
|
let fixture: ComponentFixture<PlayingCardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PlayingCardComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PlayingCardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
23
src/app/components/playing-card/playing-card.component.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import {Component, computed, input, Input, OnChanges, SimpleChanges} from '@angular/core';
|
||||||
|
import { Monster } from '../../models/monster.model';
|
||||||
|
import {MonsterTypeProperties} from '../../utils/monster.utils';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-playing-card',
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './playing-card.component.html',
|
||||||
|
styleUrl: './playing-card.component.css'
|
||||||
|
})
|
||||||
|
export class PlayingCardComponent {
|
||||||
|
|
||||||
|
monster = input(new Monster());
|
||||||
|
|
||||||
|
MonsterTypeIcon = computed(() => {
|
||||||
|
return MonsterTypeProperties[this.monster().type].imageUrl;
|
||||||
|
});
|
||||||
|
|
||||||
|
backgroundColor = computed(() => {
|
||||||
|
return MonsterTypeProperties[this.monster().type].color;
|
||||||
|
});
|
||||||
|
}
|
||||||
29
src/app/components/search-bar/search-bar.component.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#search-bar {
|
||||||
|
display: flex;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 40px;
|
||||||
|
margin: 50px auto;
|
||||||
|
box-shadow: 3px 3px 10px -3px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
all: unset;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
margin: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 40px;
|
||||||
|
background-color: rgb(205, 205, 255);
|
||||||
|
width: 50px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: rgb(179, 179, 255);
|
||||||
|
}
|
||||||
4
src/app/components/search-bar/search-bar.component.html
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<div id="search-bar">
|
||||||
|
<input type="text" placeholder="Search..." [ngModel]="search" (ngModelChange)="updateSearch($event)" />
|
||||||
|
<button (click)="searchClick()"><img src="assets/img/search.png" width="20" height="20" alt=""></button>
|
||||||
|
</div>
|
||||||
23
src/app/components/search-bar/search-bar.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SearchBarComponent } from './search-bar.component';
|
||||||
|
|
||||||
|
describe('SearchBarComponent', () => {
|
||||||
|
let component: SearchBarComponent;
|
||||||
|
let fixture: ComponentFixture<SearchBarComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SearchBarComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SearchBarComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
25
src/app/components/search-bar/search-bar.component.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Component, Output, EventEmitter, Input } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-search-bar',
|
||||||
|
standalone: true,
|
||||||
|
imports: [FormsModule],
|
||||||
|
templateUrl: './search-bar.component.html',
|
||||||
|
styleUrl: './search-bar.component.css'
|
||||||
|
})
|
||||||
|
export class SearchBarComponent {
|
||||||
|
|
||||||
|
@Input() search = '';
|
||||||
|
|
||||||
|
@Output() searchChange = new EventEmitter<string>();
|
||||||
|
@Output() searchButtonClicked = new EventEmitter<void>();
|
||||||
|
|
||||||
|
searchClick() {
|
||||||
|
this.searchButtonClicked.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSearch(value: string) {
|
||||||
|
this.searchChange.emit(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/app/guards/is-logged-in.guard.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { CanActivateFn } from '@angular/router';
|
||||||
|
|
||||||
|
import { isLoggedInGuard } from './is-logged-in.guard';
|
||||||
|
|
||||||
|
describe('isLoggedInGuard', () => {
|
||||||
|
const executeGuard: CanActivateFn = (...guardParameters) =>
|
||||||
|
TestBed.runInInjectionContext(() => isLoggedInGuard(...guardParameters));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(executeGuard).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
25
src/app/guards/is-logged-in.guard.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {CanActivateFn, Router} from '@angular/router';
|
||||||
|
import {inject} from '@angular/core';
|
||||||
|
import {LoginService} from '../services/login/login.service';
|
||||||
|
import {catchError, map} from 'rxjs';
|
||||||
|
|
||||||
|
export const isLoggedInGuard: CanActivateFn = (route, state) => {
|
||||||
|
|
||||||
|
const loginService: LoginService = inject(LoginService);
|
||||||
|
const router: Router = inject(Router);
|
||||||
|
|
||||||
|
if (loginService.user() === undefined) {
|
||||||
|
return loginService.getUsers().pipe(
|
||||||
|
map(_ => {
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
catchError(_ => router.navigate(['login']))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginService.user() === null) {
|
||||||
|
router.navigate(['login']).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
17
src/app/interceptors/auth-token.interceptor.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { HttpInterceptorFn } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { authTokenInterceptor } from './auth-token.interceptor';
|
||||||
|
|
||||||
|
describe('authTokenInterceptor', () => {
|
||||||
|
const interceptor: HttpInterceptorFn = (req, next) =>
|
||||||
|
TestBed.runInInjectionContext(() => authTokenInterceptor(req, next));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(interceptor).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
15
src/app/interceptors/auth-token.interceptor.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { HttpInterceptorFn } from '@angular/common/http';
|
||||||
|
|
||||||
|
export const authTokenInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
let requestToSend = req;
|
||||||
|
if(token) {
|
||||||
|
const headers = req.headers.set('Authorization', `Token ${token}`);
|
||||||
|
requestToSend = req.clone({
|
||||||
|
headers: headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return next(requestToSend);
|
||||||
|
};
|
||||||
13
src/app/interfaces/monster.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export interface IMonster {
|
||||||
|
id?: number
|
||||||
|
|
||||||
|
name: string
|
||||||
|
image: string
|
||||||
|
type: string
|
||||||
|
hp: number
|
||||||
|
figureCaption: string
|
||||||
|
|
||||||
|
attackName: string
|
||||||
|
attackStrength: number
|
||||||
|
attackDescription: string
|
||||||
|
}
|
||||||
4
src/app/models/credentials.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface Credentials {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
31
src/app/models/monster.model.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {MonsterType} from "../utils/monster.utils";
|
||||||
|
import {IMonster} from '../interfaces/monster';
|
||||||
|
|
||||||
|
export class Monster implements IMonster {
|
||||||
|
|
||||||
|
id: number = -1;
|
||||||
|
name: string = 'Monster';
|
||||||
|
image: string = 'assets/img/pik.webp';
|
||||||
|
type: MonsterType = MonsterType.ELECTRIC;
|
||||||
|
hp: number = 60;
|
||||||
|
figureCaption: string = 'N°001 Monster';
|
||||||
|
|
||||||
|
attackName: string = 'Standard Attack';
|
||||||
|
attackStrength: number = 10;
|
||||||
|
attackDescription: string = 'This is an attack description...';
|
||||||
|
|
||||||
|
copy(): Monster {
|
||||||
|
return Object.assign(new Monster(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(monsterJson: IMonster): Monster {
|
||||||
|
return Object.assign(new Monster(), monsterJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJson(): IMonster {
|
||||||
|
const monsterJson: IMonster = Object.assign({}, this)
|
||||||
|
delete monsterJson.id;
|
||||||
|
return monsterJson;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
5
src/app/models/user.model.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export class User {
|
||||||
|
username: string = '';
|
||||||
|
firstName: string = '';
|
||||||
|
lastName: string = '';
|
||||||
|
}
|
||||||
20
src/app/pages/login/login.component.css
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-error {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
17
src/app/pages/login/login.component.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<div id="container">
|
||||||
|
<form (submit)="login()" [formGroup]="loginFormGroup">
|
||||||
|
<h3>Login</h3>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Username</mat-label>
|
||||||
|
<input matInput formControlName="username">
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Password</mat-label>
|
||||||
|
<input matInput type="password" formControlName="password">
|
||||||
|
</mat-form-field>
|
||||||
|
<button mat-flat-button [disabled]="loginFormGroup.invalid">Login</button>
|
||||||
|
@if (invalidCredentials) {
|
||||||
|
<mat-error>Invalid credentials</mat-error>
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
23
src/app/pages/login/login.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LoginComponent } from './login.component';
|
||||||
|
|
||||||
|
describe('LoginComponent', () => {
|
||||||
|
let component: LoginComponent;
|
||||||
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [LoginComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LoginComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
65
src/app/pages/login/login.component.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import {Component, inject, OnDestroy} from '@angular/core';
|
||||||
|
import {MatError, MatFormField, MatLabel} from '@angular/material/form-field';
|
||||||
|
import {MatInput} from '@angular/material/input';
|
||||||
|
import {MatButton} from '@angular/material/button';
|
||||||
|
import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||||
|
import {LoginService} from '../../services/login/login.service';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
import {Subscription} from 'rxjs';
|
||||||
|
import {Credentials} from '../../models/credentials';
|
||||||
|
import {User} from '../../models/user.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
MatFormField,
|
||||||
|
MatLabel,
|
||||||
|
MatInput,
|
||||||
|
MatButton,
|
||||||
|
MatError,
|
||||||
|
ReactiveFormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './login.component.html',
|
||||||
|
styleUrl: './login.component.css'
|
||||||
|
})
|
||||||
|
export class LoginComponent implements OnDestroy {
|
||||||
|
|
||||||
|
private readonly formBuilder: FormBuilder = inject(FormBuilder);
|
||||||
|
private readonly loginService: LoginService = inject(LoginService);
|
||||||
|
private readonly router: Router = inject(Router);
|
||||||
|
|
||||||
|
private loginSubscription: Subscription | null = null;
|
||||||
|
|
||||||
|
loginFormGroup = this.formBuilder.group({
|
||||||
|
username: ['', [
|
||||||
|
Validators.required
|
||||||
|
]],
|
||||||
|
password: ['', [
|
||||||
|
Validators.required
|
||||||
|
]]
|
||||||
|
});
|
||||||
|
|
||||||
|
invalidCredentials = false;
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.loginSubscription?.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
login() {
|
||||||
|
this.loginSubscription = this.loginService.login(
|
||||||
|
this.loginFormGroup.value as Credentials).subscribe({
|
||||||
|
next: (result: User | null | undefined) => {
|
||||||
|
this.navigateHome();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.log(error);
|
||||||
|
this.invalidCredentials = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateHome() {
|
||||||
|
this.router.navigate(['/home']).then();
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/app/pages/monster-list/monster-list.component.css
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#card-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
margin-top: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-playing-card {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/pages/monster-list/monster-list.component.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<app-search-bar [(search)]="search" />
|
||||||
|
<div id="card-list">
|
||||||
|
@for (monster of filteredMonsters() ; track monster.name) {
|
||||||
|
<app-playing-card [monster]="monster" (click)="openMonster(monster)"/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (filteredMonsters()?.length === 0) {
|
||||||
|
<div class="center">No monsters found!</div>
|
||||||
|
} @else {
|
||||||
|
<div class="center">Found {{filteredMonsters()?.length }} monsters!</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="center">
|
||||||
|
<button mat-flat-button (click)="addMonster()">Add monster</button>
|
||||||
|
</div>
|
||||||
23
src/app/pages/monster-list/monster-list.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MonsterListComponent } from './monster-list.component';
|
||||||
|
|
||||||
|
describe('MonsterListComponent', () => {
|
||||||
|
let component: MonsterListComponent;
|
||||||
|
let fixture: ComponentFixture<MonsterListComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MonsterListComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(MonsterListComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
42
src/app/pages/monster-list/monster-list.component.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import {Component, computed, inject, model} from '@angular/core';
|
||||||
|
import {PlayingCardComponent} from "../../components/playing-card/playing-card.component";
|
||||||
|
import {SearchBarComponent} from "../../components/search-bar/search-bar.component";
|
||||||
|
import {Monster} from '../../models/monster.model';
|
||||||
|
import {MonsterService} from '../../services/monster/monster.service';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
import {toSignal} from '@angular/core/rxjs-interop';
|
||||||
|
import {MatButton} from '@angular/material/button';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-monster-list',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
PlayingCardComponent,
|
||||||
|
SearchBarComponent,
|
||||||
|
MatButton
|
||||||
|
],
|
||||||
|
templateUrl: './monster-list.component.html',
|
||||||
|
styleUrl: './monster-list.component.css'
|
||||||
|
})
|
||||||
|
export class MonsterListComponent {
|
||||||
|
|
||||||
|
private readonly router: Router = inject(Router);
|
||||||
|
private readonly monsterService: MonsterService = inject(MonsterService);
|
||||||
|
|
||||||
|
monsters = toSignal(this.monsterService.getAll());
|
||||||
|
search = model('');
|
||||||
|
|
||||||
|
filteredMonsters = computed(() => {
|
||||||
|
return this.monsters()?.filter(monster =>
|
||||||
|
monster.name.toLowerCase().includes(this.search().toLowerCase() ?? [])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
openMonster(monster: Monster) {
|
||||||
|
this.router.navigate(['/monster', monster.id]).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
addMonster() {
|
||||||
|
this.router.navigate(['/monster']).then();
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/app/pages/monster/monster.component.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
form {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ng-dirty.ng-invalid,
|
||||||
|
.ng-touched.ng-invalid {
|
||||||
|
border-color: red;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
font-size: x-small;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview, .main {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
width: 300px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.main {
|
||||||
|
width: calc(100% - 340px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container.left, .button-container.right {
|
||||||
|
display: inline-block;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container.right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
121
src/app/pages/monster/monster.component.html
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<div>
|
||||||
|
<div class="nav">
|
||||||
|
@if (monsterId >= 1) {
|
||||||
|
<p
|
||||||
|
(click)="monsterId > 1 && previous()"
|
||||||
|
class="nav-btn"
|
||||||
|
[class.disabled]="monsterId <= 1"
|
||||||
|
[attr.aria-disabled]="monsterId <= 1 ? true : null"
|
||||||
|
><<</p>
|
||||||
|
<p>Monster {{ monsterId }}</p>
|
||||||
|
} @else {
|
||||||
|
<p>No monster selected</p>
|
||||||
|
}
|
||||||
|
<p
|
||||||
|
(click)="hasNextMonster() && next()"
|
||||||
|
class="nav-btn"
|
||||||
|
[class.disabled]="!hasNextMonster()"
|
||||||
|
[attr.aria-disabled]="!hasNextMonster() ? true : null"
|
||||||
|
>>></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="preview">
|
||||||
|
<app-playing-card [monster]="monster"></app-playing-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<form [formGroup]="formGroup" (submit)="submit ($event) ">
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Name</mat-label>
|
||||||
|
<input matInput formControlName="name" id="name" name="name" type="text"/>
|
||||||
|
@if (isFieldValid('name')) {
|
||||||
|
<div class="error">This field is required.</div>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<button mat-raised-button (click)="imageInput.click()" type="button">{{imageInput.files?.[0]?.name || 'Upload file...'}}</button>
|
||||||
|
<input (change)="onFileChange($event)" #imageInput id="image" name="image" type="file" hidden/>
|
||||||
|
@if (isFieldValid('image')) {
|
||||||
|
<div class="error">This field is required.</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-form-field class="form-field">
|
||||||
|
<mat-label>Type</mat-label>
|
||||||
|
<mat-select formControlName="type" id="type" name="type">
|
||||||
|
@for (type of monsterTypes; track type) {
|
||||||
|
<mat-option [value]="type">{{ type }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>HP</mat-label>
|
||||||
|
<input matInput formControlName="hp" id="hp" name="hp" type="number"/>
|
||||||
|
@if (isFieldValid('hp')) {
|
||||||
|
@if (formGroup.get('hp')!.hasError('required')) {
|
||||||
|
<div class="error">This field is required.</div>
|
||||||
|
}
|
||||||
|
@if (formGroup.get('hp')!.hasError('min')) {
|
||||||
|
<div class="error">This field must be bigger than 0.</div>
|
||||||
|
}
|
||||||
|
@if (formGroup.get('hp')!.hasError('max')) {
|
||||||
|
<div class="error">This field must be smaller or equal to 200.</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Figure caption</mat-label>
|
||||||
|
<input matInput formControlName="figureCaption" id="figureCaption" name="figureCaption" type="text"/>
|
||||||
|
@if (isFieldValid('figureCaption')) {
|
||||||
|
<div class="error">This field is required.</div>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Attack name</mat-label>
|
||||||
|
<input matInput formControlName="attackName" id="attackName" name="attackName" type="text"/>
|
||||||
|
@if (isFieldValid('attackName')) {
|
||||||
|
<div class="error">This field is required.</div>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Attack strength</mat-label>
|
||||||
|
<input matInput formControlName="attackStrength" id="attackStrength" name="attackStrength" type="number"/>
|
||||||
|
@if (isFieldValid('attackStrength')) {
|
||||||
|
@if (formGroup.get('attackStrength')!.hasError('required')) {
|
||||||
|
<div class="error">This field is required.</div>
|
||||||
|
}
|
||||||
|
@if (formGroup.get('attackStrength')!.hasError('min')) {
|
||||||
|
<div class="error">This field must be bigger than 0.</div>
|
||||||
|
}
|
||||||
|
@if (formGroup.get('attackStrength')!.hasError('max')) {
|
||||||
|
<div class="error">This field must be smaller or equal to 200.</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Attack description</mat-label>
|
||||||
|
<input matInput formControlName="attackDescription" id="attackDescription" name="attackDescription" type="text"/>
|
||||||
|
@if (isFieldValid('attackDescription')) {
|
||||||
|
<div class="error">This field is required</div>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
<div class="button-container left">
|
||||||
|
@if (monsterId !== -1) {
|
||||||
|
<button mat-flat-button color="warn" (click)="deleteMonster()" type="button">Delete</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="button-container right">
|
||||||
|
<button mat-button (click)="navigateBack()">Back</button>
|
||||||
|
<button mat-flat-button type="submit" [disabled]="formGroup.invalid">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
23
src/app/pages/monster/monster.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MonsterComponent } from './monster.component';
|
||||||
|
|
||||||
|
describe('MonsterComponent', () => {
|
||||||
|
let component: MonsterComponent;
|
||||||
|
let fixture: ComponentFixture<MonsterComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MonsterComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(MonsterComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
175
src/app/pages/monster/monster.component.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import {Component, inject, OnDestroy, OnInit, signal} from '@angular/core';
|
||||||
|
import {ActivatedRoute, Params, Router} from '@angular/router';
|
||||||
|
import {filter, Observable, of, Subscription, switchMap} from 'rxjs';
|
||||||
|
import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||||
|
import {MonsterType} from '../../utils/monster.utils';
|
||||||
|
import {PlayingCardComponent} from '../../components/playing-card/playing-card.component';
|
||||||
|
import {Monster} from '../../models/monster.model';
|
||||||
|
import {MonsterService} from '../../services/monster/monster.service';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatFormField, MatLabel} from '@angular/material/form-field';
|
||||||
|
import {MatInput} from '@angular/material/input';
|
||||||
|
import {MatOption, MatSelect} from '@angular/material/select';
|
||||||
|
import {MatDialog} from '@angular/material/dialog';
|
||||||
|
import {
|
||||||
|
DeleteMonsterConfirmationDialogComponent
|
||||||
|
} from '../../components/delete-monster-confirmation-dialog/delete-monster-confirmation-dialog.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-monster',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
PlayingCardComponent,
|
||||||
|
MatButtonModule,
|
||||||
|
MatFormField,
|
||||||
|
MatInput,
|
||||||
|
MatLabel,
|
||||||
|
MatSelect,
|
||||||
|
MatOption
|
||||||
|
],
|
||||||
|
templateUrl: './monster.component.html',
|
||||||
|
styleUrl: './monster.component.css'
|
||||||
|
})
|
||||||
|
export class MonsterComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
private readonly route: ActivatedRoute = inject(ActivatedRoute);
|
||||||
|
private readonly router: Router = inject(Router);
|
||||||
|
private readonly fb: FormBuilder = inject(FormBuilder);
|
||||||
|
private readonly dialog: MatDialog = inject(MatDialog);
|
||||||
|
|
||||||
|
private readonly monsterService = inject(MonsterService);
|
||||||
|
|
||||||
|
subscriptions: Subscription = new Subscription();
|
||||||
|
|
||||||
|
formGroup = this.fb.group({
|
||||||
|
name: ['', [
|
||||||
|
Validators.required
|
||||||
|
]],
|
||||||
|
image: ['', [
|
||||||
|
Validators.required
|
||||||
|
]],
|
||||||
|
type: [MonsterType.ELECTRIC, [
|
||||||
|
Validators.required
|
||||||
|
]],
|
||||||
|
hp: [0, [
|
||||||
|
Validators.required,
|
||||||
|
Validators.min(1),
|
||||||
|
Validators.max(200)
|
||||||
|
]],
|
||||||
|
figureCaption: ['', [
|
||||||
|
Validators.required
|
||||||
|
]],
|
||||||
|
attackName: ['', [
|
||||||
|
Validators.required
|
||||||
|
]],
|
||||||
|
attackStrength: [0, [
|
||||||
|
Validators.required,
|
||||||
|
Validators.min(1),
|
||||||
|
Validators.max(200)
|
||||||
|
]],
|
||||||
|
attackDescription: ['', [
|
||||||
|
Validators.required
|
||||||
|
]],
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
monster = Object.assign(new Monster(), this.formGroup.value);
|
||||||
|
monsterTypes = Object.values(MonsterType);
|
||||||
|
monsterId = -1;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
const formValuesSubscription = this.formGroup.valueChanges.subscribe(data => {
|
||||||
|
this.monster = Object.assign(new Monster(), data);
|
||||||
|
});
|
||||||
|
this.subscriptions.add(formValuesSubscription);
|
||||||
|
const routeSubscription = this.route.params.pipe(
|
||||||
|
switchMap(params => {
|
||||||
|
if (params['id']) {
|
||||||
|
this.monsterId = parseInt(params['id']);
|
||||||
|
return this.monsterService.get(this.monsterId) ?? of(null)
|
||||||
|
} else {
|
||||||
|
return of(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).subscribe((monster) => {
|
||||||
|
if (monster) {
|
||||||
|
this.monster = monster;
|
||||||
|
this.formGroup.patchValue(monster);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.subscriptions.add(routeSubscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.subscriptions?.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNextMonster(): boolean {
|
||||||
|
return this.monsterService.get(this.monsterId + 1) !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
previous() {
|
||||||
|
let previousId = this.monsterId;
|
||||||
|
previousId--;
|
||||||
|
if (previousId < 1) previousId = 1;
|
||||||
|
this.router.navigate(['/monster/' + previousId]).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
next() {
|
||||||
|
let nextId = this.monsterId;
|
||||||
|
if (nextId === -1 || nextId === 0) {
|
||||||
|
nextId = 0;
|
||||||
|
}
|
||||||
|
nextId++;
|
||||||
|
this.router.navigate(['/monster/' + nextId]).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteMonster() {
|
||||||
|
const dialogRef = this.dialog.open(DeleteMonsterConfirmationDialogComponent);
|
||||||
|
const deleteSubscription = dialogRef.afterClosed().pipe(
|
||||||
|
filter(confirmation => confirmation),
|
||||||
|
switchMap(_ => this.monsterService.delete(this.monsterId))
|
||||||
|
).subscribe(_ => {
|
||||||
|
this.navigateBack();
|
||||||
|
});
|
||||||
|
this.subscriptions.add(deleteSubscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateBack() {
|
||||||
|
this.router.navigate(['/home']).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
let saveObservable: Observable<Monster>;
|
||||||
|
if (this.monsterId === -1) {
|
||||||
|
saveObservable = this.monsterService.add(this.monster);
|
||||||
|
} else {
|
||||||
|
this.monster.id = this.monsterId;
|
||||||
|
saveObservable = this.monsterService.update(this.monster);
|
||||||
|
}
|
||||||
|
const saveSubscription = saveObservable.subscribe(_ => {
|
||||||
|
this.navigateBack();
|
||||||
|
});
|
||||||
|
this.subscriptions.add(saveSubscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
isFieldValid(fieldName: string): boolean {
|
||||||
|
const formControl = this.formGroup.get(fieldName);
|
||||||
|
return formControl ? formControl.invalid && (formControl.dirty || formControl.touched) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileChange(event: any) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
if (event.target.files && event.target.files.length) {
|
||||||
|
const [file] = event.target.files;
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => {
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
image: reader.result as string
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/app/pages/not-found/not-found.component.css
Normal file
1
src/app/pages/not-found/not-found.component.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<p>not-found works!</p>
|
||||||
23
src/app/pages/not-found/not-found.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NotFoundComponent } from './not-found.component';
|
||||||
|
|
||||||
|
describe('NotFoundComponent', () => {
|
||||||
|
let component: NotFoundComponent;
|
||||||
|
let fixture: ComponentFixture<NotFoundComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [NotFoundComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(NotFoundComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
12
src/app/pages/not-found/not-found.component.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-not-found',
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './not-found.component.html',
|
||||||
|
styleUrl: './not-found.component.css'
|
||||||
|
})
|
||||||
|
export class NotFoundComponent {
|
||||||
|
|
||||||
|
}
|
||||||
16
src/app/services/login/login.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LoginService } from './login.service';
|
||||||
|
|
||||||
|
describe('LoginService', () => {
|
||||||
|
let service: LoginService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(LoginService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
51
src/app/services/login/login.service.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {inject, Injectable, signal, WritableSignal} from '@angular/core';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {User} from '../../models/user.model';
|
||||||
|
import {Credentials} from '../../models/credentials';
|
||||||
|
import {map, Observable, tap} from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class LoginService {
|
||||||
|
|
||||||
|
private readonly http: HttpClient = inject(HttpClient);
|
||||||
|
private readonly BASE_URL = 'http://localhost:8000';
|
||||||
|
|
||||||
|
user = signal<User | null | undefined>(undefined);
|
||||||
|
|
||||||
|
login(credentials: Credentials): Observable<User | null | undefined> {
|
||||||
|
return this.http.post<User>(this.BASE_URL + '/sessions/login/', credentials).pipe(
|
||||||
|
tap((result: any) => {
|
||||||
|
localStorage.setItem('token', result.token);
|
||||||
|
const user: User = Object.assign(new User(), result['user']);
|
||||||
|
this.user.set(user);
|
||||||
|
}),
|
||||||
|
map((result: any) => {
|
||||||
|
return this.user();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(): Observable<null> {
|
||||||
|
return this.http.get(this.BASE_URL + '/sessions/logout/').pipe(
|
||||||
|
tap(() => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
this.user.set(null);
|
||||||
|
}),
|
||||||
|
map(() => null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsers(): Observable<User | null | undefined> {
|
||||||
|
return this.http.get(this.BASE_URL + '/sessions/me/').pipe(
|
||||||
|
tap((result: any) => {
|
||||||
|
const user: User = Object.assign(new User(), result);
|
||||||
|
this.user.set(user);
|
||||||
|
}),
|
||||||
|
map((result: any) => {
|
||||||
|
return this.user();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/services/monster/monster.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MonsterService } from './monster.service';
|
||||||
|
|
||||||
|
describe('MonsterService', () => {
|
||||||
|
let service: MonsterService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(MonsterService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
50
src/app/services/monster/monster.service.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import {inject, Injectable} from '@angular/core';
|
||||||
|
import {Monster} from '../../models/monster.model';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {IMonster} from '../../interfaces/monster';
|
||||||
|
import {map, Observable} from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class MonsterService {
|
||||||
|
|
||||||
|
private readonly http: HttpClient = inject(HttpClient);
|
||||||
|
private readonly BASE_URL = 'http://localhost:8000/monsters/';
|
||||||
|
|
||||||
|
getAll(): Observable<Monster[]> {
|
||||||
|
return this.http.get<IMonster[]>(this.BASE_URL).pipe(
|
||||||
|
map((monsters: IMonster[]) => {
|
||||||
|
return monsters.map(monsterJson => Monster.fromJson(monsterJson));
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get(id: number): Observable<Monster> | undefined {
|
||||||
|
return this.http.get<IMonster>(this.BASE_URL + id + '/').pipe(
|
||||||
|
map((monsterJson: IMonster) => {
|
||||||
|
return Monster.fromJson(monsterJson);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
add(monster: Monster): Observable<Monster> {
|
||||||
|
return this.http.post<IMonster>(this.BASE_URL, monster.toJson()).pipe(
|
||||||
|
map((monsterJson: IMonster) => {
|
||||||
|
return Monster.fromJson(monsterJson);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(monster: Monster): Observable<Monster> {
|
||||||
|
return this.http.put<IMonster>(this.BASE_URL + monster.id + '/', monster.toJson()).pipe(
|
||||||
|
map((monsterJson: IMonster) => {
|
||||||
|
return Monster.fromJson(monsterJson);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(id: number): Observable<void> {
|
||||||
|
return this.http.delete<void>(this.BASE_URL + id + '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/app/utils/monster.utils.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export enum MonsterType {
|
||||||
|
PLANT = 'plant',
|
||||||
|
ELECTRIC = 'electric',
|
||||||
|
FIRE = 'fire',
|
||||||
|
WATER = 'water'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMonsterProperties {
|
||||||
|
imageUrl: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MonsterTypeProperties: {[key: string]: IMonsterProperties} = {
|
||||||
|
[MonsterType.PLANT]: {
|
||||||
|
imageUrl: 'assets/img/plant.png',
|
||||||
|
color: '#4CAF50'
|
||||||
|
},
|
||||||
|
[MonsterType.ELECTRIC]: {
|
||||||
|
imageUrl: 'assets/img/electric.png',
|
||||||
|
color: '#FFEB3B'
|
||||||
|
},
|
||||||
|
[MonsterType.FIRE]: {
|
||||||
|
imageUrl: 'assets/img/fire.png',
|
||||||
|
color: '#FF5722'
|
||||||
|
},
|
||||||
|
[MonsterType.WATER]: {
|
||||||
|
imageUrl: 'assets/img/water.png',
|
||||||
|
color: '#2196F3'
|
||||||
|
}
|
||||||
|
};
|
||||||
15
src/index.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>PlayingCards</title>
|
||||||
|
<base href="/">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="mat-typography">
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6
src/main.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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));
|
||||||
9
src/styles.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
body, html {
|
||||||
|
font-family: Roboto, sans-serif;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
545deg,
|
||||||
|
rgb(252, 228, 252),
|
||||||
|
rgb(218, 228, 255)
|
||||||
|
);
|
||||||
|
}
|
||||||
15
tsconfig.app.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/main.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
33
tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/out-tsc",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": false,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
15
tsconfig.spec.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/spec",
|
||||||
|
"types": [
|
||||||
|
"jasmine"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||