Compare commits

..

17 commits

Author SHA1 Message Date
a09d4ee75d Services: change api url 2025-02-07 21:51:12 +01:00
c2c008c952 Components/create-edit-modal: change number of required images 2025-02-07 18:59:27 +01:00
d7c9e892d4 Services: add genre service 2025-02-07 18:53:29 +01:00
70f7738966 Interfaces: add genres to api responses 2025-02-07 18:34:35 +01:00
1dd0333bc3 Interfaces: make names generic 2025-02-07 18:28:28 +01:00
b44fe2e51d Interfaces: add genre 2025-02-07 14:26:15 +01:00
747e733574 Pages: make genres page 2025-02-07 14:12:05 +01:00
13153fc9d4 Pages/shows: fix card grid 2025-02-06 20:12:08 +01:00
1c9b1487de Components/create-edit-modal: change url validation regex 2025-02-06 19:52:37 +01:00
a52a76b6d0 Components/create-edit-modal: send and recieve images from api 2025-02-06 19:17:51 +01:00
cae2fd1e09 Interfaces/show: add images 2025-02-06 19:17:18 +01:00
455e2869a5 Components/create-edit-modal: make first image mandatory 2025-02-06 19:01:54 +01:00
f839706d05 Components/create-edit-modal: add validation to image controls 2025-02-06 18:45:45 +01:00
b2cfda8c32 Components/create-edit-modal: add controls for adding and removing images 2025-02-06 18:42:26 +01:00
aec0a34f71 Components/create-edit-modal: add floating labels 2025-02-06 18:20:50 +01:00
58bb2cb3ca Components/create-edit-modal: don't add date form control separately 2025-02-06 17:26:32 +01:00
2de5b804af Components: fix some components being double nested 2025-02-06 17:00:23 +01:00
29 changed files with 266 additions and 105 deletions

View file

@ -1,6 +1,6 @@
import {Component} from '@angular/core';
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
import {ToastContainerComponent} from '../components/toast-container/toast-container/toast-container.component';
import {ToastContainerComponent} from '../components/toast-container/toast-container.component';
import {NgbCollapse} from '@ng-bootstrap/ng-bootstrap';
import {routes} from './app.routes';

View file

@ -1,5 +1,6 @@
import {Routes} from '@angular/router';
import {ShowsComponent} from '../pages/shows/shows.component';
import {GenresComponent} from '../pages/genres/genres.component';
export const routes: Routes = [
{
@ -10,5 +11,9 @@ export const routes: Routes = [
{
path: 'shows',
component: ShowsComponent
},
{
path: 'genres',
component: GenresComponent
}
];

View file

@ -0,0 +1,62 @@
<div class="modal-header">
<h4 class="modal-title">
@if (editMode) {
Edit show
} @else {
Add new show
}
</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="dismiss()"></button>
</div>
<form [formGroup]="newShowForm" (submit)="formSubmitted(newShowForm)">
<div class="modal-body">
<div class="mb-3 form-floating">
<input formControlName="title" type="text" class="form-control" placeholder=""/>
<label class="form-label">Title</label>
</div>
<div class="mb-3 form-floating">
<input formControlName="date" type="date" class="form-control" placeholder=""/>
<label class="form-label">Date</label>
</div>
<div class="mb-3 form-floating">
<input formControlName="seasons" type="number" class="form-control" placeholder=""/>
<label class="form-label">Seasons</label>
</div>
<div class="mb-3 form-floating">
<input formControlName="episodes" type="number" class="form-control" placeholder=""/>
<label class="form-label">Episodes</label>
</div>
<div class="mb-3 form-floating">
<input formControlName="description" type="text" class="form-control" placeholder=""/>
<label class="form-label">Description</label>
</div>
<label class="form-label">Images</label>
<div class="card" formArrayName="images">
<ul class="list-group list-group-flush">
@for (imageControl of images.controls; track imageControl) {
<li class="list-group-item">
<div class="input-group">
<div class="form-floating">
<input formControlName="{{$index}}" type="text" class="form-control" placeholder="">
<label class="form-label">Image {{ $index + 1 }} URL</label>
</div>
@if ($index >= requiredImages) {
<button type="button" class="btn btn-outline-danger" (click)="removeImageControl($index)">
<i class="bi bi-x-lg"></i>
</button>
}
</div>
</li>
}
<li class="list-group-item">
<button type="button" class="btn btn-outline-primary" (click)="addImageControl()">
<i class="bi bi-plus-lg"></i> Add new image
</button>
</li>
</ul>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-success" [disabled]="newShowForm.invalid">Submit</button>
</div>
</form>

View file

@ -1,12 +1,12 @@
import {Component, inject} from '@angular/core';
import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {ShowsApiService} from '../../../services/shows/shows-api.service';
import {ShowsApiCreation} from '../../../interfaces/shows-api-creation';
import {ToastService} from '../../../services/toast/toast.service';
import {Show} from '../../../interfaces/show';
import {FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {ShowsApiService} from '../../services/shows/shows-api.service';
import {ApiCreationResponse} from '../../interfaces/api-creation-response';
import {ToastService} from '../../services/toast/toast.service';
import {Show} from '../../interfaces/show';
import {formatDate} from '@angular/common';
import {ShowsApiDeletionEdit} from '../../../interfaces/shows-api-deletion-edit';
import {ApiDeletionEditResponse} from '../../interfaces/api-deletion-edit-response';
@Component({
selector: 'app-create-edit-modal',
@ -24,31 +24,59 @@ export class CreateEditModalComponent {
protected editMode: boolean = false
protected show?: Show
protected requiredImages: number
constructor() {
this.requiredImages = 3
this.initForm()
}
private initForm() {
let formattedDate: string
this.newShowForm = new FormGroup({
title: new FormControl(this.show?.title, Validators.required),
seasons: new FormControl(this.show?.seasons, [Validators.required, Validators.min(1)]),
episodes: new FormControl(this.show?.episodes, [Validators.required, Validators.min(1)]),
description: new FormControl(this.show?.description, Validators.required)
})
if (this.show?.date !== undefined) {
formattedDate = formatDate(this.show?.date, "YYYY-MM-dd", "en")
} else {
formattedDate = ""
}
this.newShowForm.addControl("date", new FormControl(formattedDate, Validators.required))
this.newShowForm = new FormGroup({
title: new FormControl(this.show?.title, Validators.required),
seasons: new FormControl(this.show?.seasons, [Validators.required, Validators.min(1)]),
date: new FormControl(formattedDate, Validators.required),
episodes: new FormControl(this.show?.episodes, [Validators.required, Validators.min(1)]),
description: new FormControl(this.show?.description, Validators.required),
images: new FormArray([])
})
if (this.show?.images !== undefined) {
this.show?.images.forEach((imageUrl: string) => {
this.addImageControl(imageUrl)
})
} else {
let i: number = this.requiredImages
do {
this.addImageControl()
i--
} while (i != 0)
}
}
get images(): FormArray {
return this.newShowForm.get("images") as FormArray
}
protected dismiss() {
this.activeModal.dismiss()
}
protected addImageControl(value: string = "") {
const urlRegex = "(https?:\\/\\/(?:www\\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|www\\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|https?:\\/\\/(?:www\\.|(?!www))[a-zA-Z0-9]+\\.[^\\s]{2,}|www\\.[a-zA-Z0-9]+\\.[^\\s]{2,})"
this.images.push(new FormControl(value, [Validators.required, Validators.pattern(urlRegex)]))
}
protected removeImageControl(index: number) {
this.images.removeAt(index)
}
protected formSubmitted(form: FormGroup) {
let show: Show = {
title: form.get("title")?.value,
@ -56,14 +84,14 @@ export class CreateEditModalComponent {
seasons: form.get("seasons")?.value,
episodes: form.get("episodes")?.value,
description: form.get("description")?.value,
images: form.get("images")?.value,
//TODO: Allow user to specify genres
genres: []
}
if (!this.editMode) {
this.showsService.sendShow(show).subscribe({
next: (response: ShowsApiCreation) => {
next: (response: ApiCreationResponse) => {
show._id = response.newId
}, error: err => {
this.showToast(true, "created")
@ -78,7 +106,7 @@ export class CreateEditModalComponent {
// I add it here
show._id = this.show?._id
this.showsService.updateShow(show).subscribe({
next: (response: ShowsApiDeletionEdit) => {
next: (response: ApiDeletionEditResponse) => {
// Do nothing
}, error: err => {
this.showToast(true, "edited")

View file

@ -1,38 +0,0 @@
<div class="modal-header">
<h4 class="modal-title">
@if (editMode) {
Edit show
} @else {
Add new show
}
</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="dismiss()"></button>
</div>
<form [formGroup]="newShowForm" (submit)="formSubmitted(newShowForm)">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Title</label>
<input formControlName="title" type="text" class="form-control"/>
</div>
<div class="mb-3">
<label class="form-label">Date</label>
<input formControlName="date" type="date" class="form-control"/>
</div>
<div class="mb-3">
<label class="form-label">Seasons</label>
<input formControlName="seasons" type="number" class="form-control"/>
</div>
<div class="mb-3">
<label class="form-label">Episodes</label>
<input formControlName="episodes" type="number" class="form-control"/>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<input formControlName="description" type="text" class="form-control"/>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-success" [disabled]="newShowForm.invalid">Submit</button>
</div>
</form>

View file

@ -1,5 +1,5 @@
import {Component, inject} from '@angular/core';
import {ToastService} from '../../../services/toast/toast.service';
import {ToastService} from '../../services/toast/toast.service';
import {NgbToast} from '@ng-bootstrap/ng-bootstrap';
@Component({

View file

@ -1,4 +1,4 @@
export interface ShowsApiCreation {
export interface ApiCreationResponse {
status: string
message: string
newId: string

View file

@ -0,0 +1,4 @@
export interface ApiDeletionEditResponse {
status: string
message: string
}

View file

@ -0,0 +1,8 @@
import {Show} from './show';
import {Genre} from './genre';
export interface ApiIdResponse {
status: string
show?: Show
genre?: Genre
}

View file

@ -0,0 +1,9 @@
import {Show} from './show';
import {Genre} from './genre';
export interface ApiResponse {
status: string
shows?: Show[]
genres?: Genre[]
totalShows: number
}

6
src/interfaces/genre.ts Normal file
View file

@ -0,0 +1,6 @@
export interface Genre {
// ID is assigned by the DB, so I don't want to have to specify it
_id?: string
name: string
showIDs: string[]
}

View file

@ -7,4 +7,5 @@ export interface Show {
episodes: number
description: string
genres: string[]
images: string[]
}

View file

@ -1,4 +0,0 @@
export interface ShowsApiDeletionEdit {
status: string
message: string
}

View file

@ -1,6 +0,0 @@
import {Show} from './show';
export interface ShowsApiIdResponse {
status: string
show: Show
}

View file

@ -1,7 +0,0 @@
import {Show} from './show';
export interface ShowsApiResponse {
status: string
shows: Show[]
totalShows: number
}

View file

View file

@ -0,0 +1 @@
<p>genres works!</p>

View file

@ -0,0 +1,23 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {GenresComponent} from './genres.component';
describe('CategoriesComponent', () => {
let component: GenresComponent;
let fixture: ComponentFixture<GenresComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GenresComponent]
})
.compileComponents();
fixture = TestBed.createComponent(GenresComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,11 @@
import {Component} from '@angular/core';
@Component({
selector: 'app-genres',
imports: [],
templateUrl: './genres.component.html',
styleUrl: './genres.component.css'
})
export class GenresComponent {
}

View file

@ -1,17 +1,19 @@
<div class="container">
<div class="row row-cols-4">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-4">
@for (show of shows; track show._id) {
<div class="card col">
<!-- <img src="">-->
<div class="card-body">
<h5 class="card-title">{{ show.title }}</h5>
<p class="card-text">{{ show.date | date }}</p>
<button type="button" class="btn btn-outline-danger" (click)="deleteShow(show)">
<i class="bi bi-trash"></i>
</button>
<button type="button" class="btn btn-outline-warning" (click)="editShow(show, $index)">
<i class="bi bi-pencil"></i>
</button>
<div class="col">
<div class="card h-100">
<!-- <img src="">-->
<div class="card-body">
<h5 class="card-title">{{ show.title }}</h5>
<p class="card-text">{{ show.date | date }}</p>
<button type="button" class="btn btn-outline-danger" (click)="deleteShow(show)">
<i class="bi bi-trash"></i>
</button>
<button type="button" class="btn btn-outline-warning" (click)="editShow(show, $index)">
<i class="bi bi-pencil"></i>
</button>
</div>
</div>
</div>
}

View file

@ -1,11 +1,11 @@
import {Component, inject} from '@angular/core';
import {ShowsApiService} from '../../services/shows/shows-api.service';
import {Show} from '../../interfaces/show';
import {ShowsApiResponse} from '../../interfaces/shows-api-response';
import {ApiResponse} from '../../interfaces/api-response';
import {Toast} from '../../interfaces/toast';
import {ToastService} from '../../services/toast/toast.service';
import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap';
import {CreateEditModalComponent} from '../../components/create-modal/create-edit-modal/create-edit-modal.component';
import {CreateEditModalComponent} from '../../components/create-edit-modal/create-edit-modal.component';
import {DatePipe} from '@angular/common';
import {DeleteModalComponent} from '../../components/delete-modal/delete-modal.component';
@ -29,8 +29,8 @@ export class ShowsComponent {
let loadToast: Toast = {body: "Loading shows..."};
this.toastService.show(loadToast);
this.api.getShows().subscribe({
next: (response: ShowsApiResponse) => {
this.shows = response.shows;
next: (response: ApiResponse) => {
this.shows = response.shows ?? [];
}, error: (err: any) => {
console.error("Error: ", err);
}, complete: () => {

View file

@ -0,0 +1,16 @@
import {TestBed} from '@angular/core/testing';
import {GenresService} from './genres.service';
describe('GenresService', () => {
let service: GenresService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(GenresService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View file

@ -0,0 +1,40 @@
import {inject, Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {ApiResponse} from '../../interfaces/api-response';
import {ApiIdResponse} from '../../interfaces/api-id-response';
import {Show} from '../../interfaces/show';
import {ApiCreationResponse} from '../../interfaces/api-creation-response';
import {ApiDeletionEditResponse} from '../../interfaces/api-deletion-edit-response';
@Injectable({
providedIn: 'root'
})
export class GenresService {
private readonly url: string = "https://shows.toast003.xyz/api/";
private genresEndpoint: string
private idEndpoint: string
private http: HttpClient = inject(HttpClient);
constructor() {
this.genresEndpoint = this.url + "genres/"
this.idEndpoint = this.genresEndpoint + "id/"
}
getGenres(): Observable<ApiResponse> {
return this.http.get<ApiResponse>(this.genresEndpoint)
}
getGenre(id: string): Observable<ApiIdResponse> {
return this.http.get<ApiIdResponse>(this.idEndpoint + id)
}
sendGenre(newShow: Show): Observable<ApiCreationResponse> {
return this.http.post<ApiCreationResponse>(this.genresEndpoint, newShow)
}
updateGenre(show: Show): Observable<ApiDeletionEditResponse> {
return this.http.put<ApiDeletionEditResponse>(this.idEndpoint + show._id, show)
}
}

View file

@ -1,18 +1,18 @@
import {inject, Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {ShowsApiResponse} from '../../interfaces/shows-api-response';
import {ShowsApiCreation} from '../../interfaces/shows-api-creation';
import {ShowsApiIdResponse} from '../../interfaces/shows-api-id-response';
import {ShowsApiDeletionEdit} from '../../interfaces/shows-api-deletion-edit';
import {ApiResponse} from '../../interfaces/api-response';
import {ApiIdResponse} from '../../interfaces/api-id-response';
import {ApiDeletionEditResponse} from '../../interfaces/api-deletion-edit-response';
import {Show} from '../../interfaces/show';
import {ApiCreationResponse} from '../../interfaces/api-creation-response';
@Injectable({
providedIn: 'root'
})
export class ShowsApiService {
private readonly url: string = "https://shows.everest.tailscale/api/";
private readonly url: string = "https://shows.toast003.xyz/api/";
private showsEndpoint: string
private idEndpoint: string
private http: HttpClient = inject(HttpClient);
@ -22,23 +22,23 @@ export class ShowsApiService {
this.idEndpoint = this.showsEndpoint + "id/"
}
getShows(): Observable<ShowsApiResponse> {
return this.http.get<ShowsApiResponse>(this.showsEndpoint)
getShows(): Observable<ApiResponse> {
return this.http.get<ApiResponse>(this.showsEndpoint)
}
getShow(id: string): Observable<ShowsApiIdResponse> {
return this.http.get<ShowsApiIdResponse>(this.idEndpoint + id)
getShow(id: string): Observable<ApiIdResponse> {
return this.http.get<ApiIdResponse>(this.idEndpoint + id)
}
sendShow(newShow: Show): Observable<ShowsApiCreation> {
return this.http.post<ShowsApiCreation>(this.showsEndpoint, newShow)
sendShow(newShow: Show): Observable<ApiCreationResponse> {
return this.http.post<ApiCreationResponse>(this.showsEndpoint, newShow)
}
deleteShw(id: string): Observable<ShowsApiDeletionEdit> {
return this.http.delete<ShowsApiDeletionEdit>(this.idEndpoint + id)
deleteShw(id: string): Observable<ApiDeletionEditResponse> {
return this.http.delete<ApiDeletionEditResponse>(this.idEndpoint + id)
}
updateShow(show: Show): Observable<ShowsApiDeletionEdit> {
return this.http.put<ShowsApiDeletionEdit>(this.idEndpoint + show._id, show)
updateShow(show: Show): Observable<ApiDeletionEditResponse> {
return this.http.put<ApiDeletionEditResponse>(this.idEndpoint + show._id, show)
}
}