diff --git a/db.json b/db.json index 0967ef4..e36e314 100644 --- a/db.json +++ b/db.json @@ -1 +1,50 @@ -{} +{ + "donuts": [ + { + "id": "xy12yr", + "name": "Just Chocolate", + "icon": "just-chocolate", + "price": 120, + "promo": "new", + "description": "For the pure chocoholic." + }, + { + "id": "zy19yr", + "name": "Glazed Fudge", + "icon": "glazed-fudge", + "price": 129, + "promo": "new", + "description": "Sticky perfection." + }, + { + "id": "qy19ya", + "name": "Caramel Swirl", + "icon": "caramel-swirl", + "price": 129, + "promo": "new", + "description": "Chocolate drizzled with caramel" + }, + { + "id": "2dfe5", + "name": "Sour Supreme", + "icon": "sour-supreme", + "price": 139, + "description": "For the sour advocate." + }, + { + "id": "a6rfg", + "name": "Zesty Lemon", + "icon": "zesty-lemon", + "price": 129, + "description": "Delicious luscious lemon." + }, + { + "name": "teestfff", + "icon": "glazed-fudge", + "price": 3, + "promo": "limited", + "description": "qdsqsdf", + "id": "RF-n7_z" + } + ] +} \ No newline at end of file diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts new file mode 100644 index 0000000..d9867a0 --- /dev/null +++ b/src/app/admin/admin.module.ts @@ -0,0 +1,37 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormsModule} from "@angular/forms"; +import {RouterModule, Routes} from "@angular/router"; + +// containers +import {DonutListComponent} from './containers/donut-list/donut-list.component'; +import {DonutSingleComponent} from './containers/donut-single/donut-single.component'; + +// components +import {DonutCardComponent} from './components/donut-card/donut-card.component'; +import {DonutFormComponent} from './components/donut-form/donut-form.component'; + +export const routes: Routes = [ + {path: 'donuts', component: DonutListComponent}, + {path: 'donuts/new', component: DonutSingleComponent, data: {isEdit: false}}, + {path: 'donuts/:id', component: DonutSingleComponent, data: {isEdit: true}}, + // redirects http://localhost:4200/admin to http://localhost:4200/admin/donuts + // put always at the end + {path: '', pathMatch: 'full', redirectTo: 'donuts'}, +]; + +@NgModule({ + declarations: [ + DonutListComponent, + DonutSingleComponent, + DonutCardComponent, + DonutFormComponent, + ], + imports: [ + CommonModule, FormsModule, RouterModule.forChild(routes) + ] +}) +export class AdminModule { +} + + diff --git a/src/app/admin/components/donut-card/donut-card.component.ts b/src/app/admin/components/donut-card/donut-card.component.ts new file mode 100644 index 0000000..79dd393 --- /dev/null +++ b/src/app/admin/components/donut-card/donut-card.component.ts @@ -0,0 +1,78 @@ +import {Component, Input, ViewEncapsulation} from '@angular/core'; +import {Donut} from "../../models/donut.model"; + +@Component({ + selector: 'donut-card', + encapsulation: ViewEncapsulation.Emulated, // Is de default dat wil zeggen dat nu de css van styles.scss genomen wordt + template: ` + + +
+

+ {{ donut.name }} + + + NEW + LIMITED + Nothing special... + + +

+

+ {{ donut.price / 100 | currency }} +

+
+
+ `, + styles: [ + ` + .donut-card { + display: flex; + align-items: center; + background: #f7f7f7; + border-radius: 5px; + margin-bottom: 5px; + padding: 5px 15px; + transition: transform 0.2s ease-in-out; + &:hover { + transform: translateY(-3px); + } + &-name { + font-size: 16px; + } + &-label { + border: 1px solid #c14583; + border-radius: 4px; + padding: 0 4px; + margin-left: 5px; + font-size: 12px; + color: #c14583; + } + &-price { + font-size: 14px; + color: #c14583; + } + &-promo { + border: 2px solid #eee; + } + &-icon { + width: 50px; + margin-right: 10px; + } + } + `, + ] +}) +export class DonutCardComponent { + @Input() donut!: Donut; +} diff --git a/src/app/admin/components/donut-form/donut-form.component.ts b/src/app/admin/components/donut-form/donut-form.component.ts new file mode 100644 index 0000000..392f3a4 --- /dev/null +++ b/src/app/admin/components/donut-form/donut-form.component.ts @@ -0,0 +1,197 @@ +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {NgForm} from "@angular/forms"; +import {Donut} from "../../models/donut.model"; + +@Component({ + selector: 'donut-form', + template: ` +
+ + + + + + +
+

Promo:

+ + + +
+ + + + + + + + +
+ Working... +
+
+ + Loading... + `, + styles: [ + ` + .donut-form { + &-radios { + display: flex; + align-content: center; + + &-label { + margin-right: 10px; + } + + label { + display: flex; + align-items: center; + + span { + color: #444; + margin-bottom: 0; + } + } + } + + &-working { + font-size: 12px; + font-style: italic; + margin: 10px 0; + } + + &-error { + font-size: 12px; + color: #e66262; + } + } + ` + ] +}) +export class DonutFormComponent { + + @Input() donut!: Donut; + @Input() isEdit!: boolean; + + @Output() create = new EventEmitter() + @Output() update = new EventEmitter() + @Output() delete = new EventEmitter() + + icons: string[] = [ + 'caramel-swirl', + 'glazed-fudge', + 'just-chocolate', + 'sour-supreme', + 'strawberry-glaze', + 'vanilla-sundae', + 'zesty-lemon', + ] + + constructor() { + } + + handleCreate(form: NgForm) { + if (form.valid) { + this.create.emit(form.value) + } else { + form.form.markAllAsTouched() + } + } + + handleUpdate(form: NgForm) { + if (form.valid) { + this.update.emit({id: this.donut.id, ...form.value}) + } else { + form.form.markAllAsTouched() + } + } + + handleDelete() { + if (confirm(`Really delete ${this.donut.name}?`)) + this.delete.emit({...this.donut}) + } +} diff --git a/src/app/admin/containers/donut-list/donut-list.component.ts b/src/app/admin/containers/donut-list/donut-list.component.ts new file mode 100644 index 0000000..6c02a44 --- /dev/null +++ b/src/app/admin/containers/donut-list/donut-list.component.ts @@ -0,0 +1,52 @@ +import {Component, OnInit} from '@angular/core'; + +import { Donut } from "../../models/donut.model"; +import {DonutService} from "../../services/donut.service"; + +@Component({ + selector: 'donut-list', + template: ` +
+ + + + + + + +

No Donust here..

+
+
+ `, + styles: [` + .donut-list { + &-actions { + margin-bottom: 10px; + } + } + `, + ], +}) +export class DonutListComponent implements OnInit { + // Declaratie hier maar vullen in ngOnInit() + donuts!: Donut[]; + + constructor(private donutService: DonutService) {} + ngOnInit(): void { + this.donutService + .read() + .subscribe((donuts: Donut[]) => (this.donuts = donuts)); + } + + trackById(index: number, value: Donut) { + return value.id; + + } +} diff --git a/src/app/admin/containers/donut-single/donut-single.component.ts b/src/app/admin/containers/donut-single/donut-single.component.ts new file mode 100644 index 0000000..7135265 --- /dev/null +++ b/src/app/admin/containers/donut-single/donut-single.component.ts @@ -0,0 +1,59 @@ +import {Component, OnInit} from '@angular/core'; +import {Donut} from "../../models/donut.model"; +import {DonutService} from "../../services/donut.service"; +import {ActivatedRoute, Router} from "@angular/router"; + +@Component({ + selector: 'donut-single', + template: ` +
+ +
+ `, + styles: [] +}) +export class DonutSingleComponent implements OnInit { + + donut!: Donut; + isEdit!: boolean; + + constructor( + private route: ActivatedRoute, + private router: Router, + private donutService: DonutService, + ) {} + + ngOnInit(): void { + const id = this.route.snapshot.paramMap.get('id'); // id refers t: { path: 'donuts/:id', component: DonutSingleComponent }, in AdminModule + console.log(id); + this.donutService + .readOne(id) + .subscribe((donut: Donut) => (this.donut = donut)); + this.isEdit = this.route.snapshot.data['isEdit']; + } + + onCreate(donut: Donut) { + this.donutService + .create(donut) + .subscribe((donut) => this.router.navigate(['admin', 'donuts', donut.id])); + } + + onUpdate(donut: Donut) { + this.donutService.update(donut).subscribe({ + next: () => this.router.navigate(['admin']), + error: (err) => console.log('onUpdate error:', err), + }); + } + + onDelete(donut: Donut) { + this.donutService + .delete(donut) + .subscribe(()=> this.router.navigate(['admin'])); + } +} diff --git a/src/app/admin/models/donut.model.ts b/src/app/admin/models/donut.model.ts new file mode 100644 index 0000000..bce3e22 --- /dev/null +++ b/src/app/admin/models/donut.model.ts @@ -0,0 +1,8 @@ +export interface Donut { + id?: string; + name: string; + icon: string; + price: number; + promo?: 'new' | 'limited'; + description: string; +} diff --git a/src/app/admin/services/donut.service.spec.ts b/src/app/admin/services/donut.service.spec.ts new file mode 100644 index 0000000..3e1e36f --- /dev/null +++ b/src/app/admin/services/donut.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { DonutService } from './donut.service'; + +describe('DonutService', () => { + let service: DonutService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DonutService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/admin/services/donut.service.ts b/src/app/admin/services/donut.service.ts new file mode 100644 index 0000000..6b6cd4f --- /dev/null +++ b/src/app/admin/services/donut.service.ts @@ -0,0 +1,99 @@ +import {Injectable} from '@angular/core'; +import {HttpClient, HttpErrorResponse, HttpHeaders} from "@angular/common/http"; + +import {tap, of, map, catchError, throwError, retryWhen, delay, take} from "rxjs"; + +import {Donut} from "../models/donut.model"; + +@Injectable({ + providedIn: 'root' +}) +export class DonutService { + private donuts: Donut[] = [] + + constructor(private http: HttpClient) { + } + + read() { + if (this.donuts.length) { + return of(this.donuts); + } + + let headers = new HttpHeaders({ + 'Content-Type': 'application/json', + }); + + headers = headers.append('Api-Token', '123abc'); + + const options = { + headers, + } + + return this.http.get(`/api/donuts`, options).pipe( + tap((donuts) => { + this.donuts = donuts; + }), + retryWhen((errors) => errors.pipe(delay(5000), take(4))), + catchError(this.handleError) + ); + } + + readOne(id: string | null) { + return this.read().pipe( + map((donuts) => { + const donut = donuts.find((donut: Donut) => donut.id === id); + if (donut) { + return donut; + } + return {name: '', icon: '', price: 0, description: ''}; + }), + catchError(this.handleError) + ); + } + + create(payload: Donut) { + return this.http.post(`/api/donuts`, payload) + .pipe( + tap((donut) => { + this.donuts = [...this.donuts, donut] + }), + catchError(this.handleError) + ) + } + + update(payload: Donut) { + return this.http.put(`/api/donuts/${payload.id}`, payload) + .pipe( + tap((donut) => { + this.donuts = this.donuts.map((item: Donut) => { + if (item.id === donut.id) { + return donut; + } + return item; + }); + }), + catchError(this.handleError) + ); + } + + delete(payload: Donut) { + return this.http.delete(`/api/donuts/${payload.id}`) + .pipe( + tap(() => { + this.donuts = this.donuts.filter((donut: Donut) => donut.id !== payload.id); + }), + catchError(this.handleError) + ) + } + + private handleError(err: HttpErrorResponse) { + if (err.error instanceof ErrorEvent) { + // client-side + console.warn('Client', err.message); + } else { + // server-side + console.warn('Server', err.status); + } + return throwError(() => new Error(err.message)); + } +} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c11bb60..91af124 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,21 +1,54 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit} from '@angular/core'; @Component({ selector: 'app-root', - template: `
Hello Angular!
`, + template: ` +
+
+ +
+ +
`, styles: [ ` .app { - margin-top: 50px; - font-size: 22px; - color: #fff; - text-align: center; + background: #fff; + border-radius: 8px; + max-width: 400px; + width: 94%; + margin: 25px auto; + padding: 25px; + border: 4px solid #ef9fc7; + } + + .header { + display: flex; + justify-content: center; + margin-bottom: 25px; + } + + .logo { + width: 100px; + height: 88px; } `, ], }) export class AppComponent implements OnInit { + message!: string; + newMessage!: string; + ngOnInit() { - console.log('Hello World!'); + this.message = 'Value on init'; + } + + handleClick(event: MouseEvent) { + console.log(event); + } + + handleInput(event: Event) { + const {value} = event.target as HTMLInputElement; + console.log(value); + this.newMessage = value; } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index bde0672..fb2d41c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,11 +1,36 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; +import {RouterModule, Routes} from "@angular/router"; +import {HttpClientModule} from "@angular/common/http"; import { AppComponent } from './app.component'; +import { AdminModule } from './admin/admin.module'; +import {DonutListComponent} from "./admin/containers/donut-list/donut-list.component"; +import {DonutSingleComponent} from "./admin/containers/donut-single/donut-single.component"; +// path: '' is http://localhost:4200/ +export const routes: Routes = [ + { + path: 'admin', + loadChildren: () => import('./admin/admin.module').then(x => x.AdminModule), + }, + { + // redirects http://localhost:4200/ to http://localhost:4200/admin + // put always at the end + path: '', + pathMatch: 'full', + redirectTo: 'admin', + }, + { + // Wild card selector redirects misspelled paths + // eg. http://localhost:4200/addddminnnn to http://localhost:4200/admin + path: '**', + redirectTo: 'admin', + }, +]; @NgModule({ declarations: [AppComponent], - imports: [BrowserModule], + imports: [BrowserModule, HttpClientModule, RouterModule.forRoot(routes)], bootstrap: [AppComponent], }) export class AppModule {}