diff options
author | sowgro <tpoke.ferrari@gmail.com> | 2025-04-06 15:15:47 -0400 |
---|---|---|
committer | sowgro <tpoke.ferrari@gmail.com> | 2025-04-06 15:15:47 -0400 |
commit | 9f14b3787a8cfc49fd168b1242adcc6d5fa8bfd6 (patch) | |
tree | 8af111a538a8d6361e1cf07467b9c31568284921 /ufund-ui | |
parent | 1ac878b4aaa19ab889c7a98b7dab6acd57c778b3 (diff) | |
parent | 04cb51b2e7891785c956c5faa73fb88cc04e82e0 (diff) | |
download | JellySolutions-9f14b3787a8cfc49fd168b1242adcc6d5fa8bfd6.tar.gz JellySolutions-9f14b3787a8cfc49fd168b1242adcc6d5fa8bfd6.tar.bz2 JellySolutions-9f14b3787a8cfc49fd168b1242adcc6d5fa8bfd6.zip |
Merge branch 'main' into light-mode
# Conflicts:
# ufund-ui/src/app/app.component.html
# ufund-ui/src/app/components/funding-basket/funding-basket.component.html
# ufund-ui/src/app/components/need-list/need-list.component.html
# ufund-ui/src/app/components/need-page/need-page.component.css
Diffstat (limited to 'ufund-ui')
36 files changed, 913 insertions, 857 deletions
diff --git a/ufund-ui/src/app/app.component.css b/ufund-ui/src/app/app.component.css index 6cb44f6..2ed8131 100644 --- a/ufund-ui/src/app/app.component.css +++ b/ufund-ui/src/app/app.component.css @@ -4,6 +4,26 @@ height: 100%; } +#scroll { + overflow: auto; + min-height: 100%; + display: flex; + flex-direction: column; +} + +.modal { + position: absolute; + background-color: #000000a8; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + justify-content: center; + align-items: center; + z-index: 3; +} + #header { display: flex; flex-direction: row; diff --git a/ufund-ui/src/app/app.component.html b/ufund-ui/src/app/app.component.html index a6e6e6a..8a0fd04 100644 --- a/ufund-ui/src/app/app.component.html +++ b/ufund-ui/src/app/app.component.html @@ -1,18 +1,23 @@ -<div id="header"> - <div> - <a routerLink="/"> - <h1>Jelly Solutions</h1> - </a> +<div id="scroll"> + <div id="header"> + <div> + <a routerLink="/"> + <h1>Jelly Solutions</h1> + </a> + </div> + <div> + <button *ngIf="(currentUser | async)?.type === userType.MANAGER" routerLink="/dashboard">Dashboard</button> + <button routerLink="/cupboard">Cupboard</button> + <button *ngIf="(currentUser | async)?.type === userType.HELPER" routerLink="/basket">Basket</button> + <!-- <span>{{currentUser$ | async}}</span>--> + <button *ngIf="currentUser | async" (click)="logout()">Log Out</button> + <button *ngIf="!(currentUser | async)" (click)="login()">Log In</button> + <button class="button" (click)="toggleColorScheme()"><span class="icon">brightness_7</span></button> + </div> </div> - <div> - <button *ngIf="(currentUser | async)?.type === userType.MANAGER" routerLink="/dashboard">Dashboard</button> - <button routerLink="/cupboard">Cupboard</button> - <button *ngIf="(currentUser | async)?.type === userType.HELPER" routerLink="/basket">Basket</button> -<!-- <span>{{currentUser$ | async}}</span>--> - <button *ngIf="currentUser | async" (click)="logout()"> Log Out</button> - <button *ngIf="!(currentUser | async)" (click)="login()"> Log In</button> - <button class="button" (click)="toggleColorScheme()"><span class="icon">brightness_7</span></button> - </div> -</div> -<router-outlet /> + <router-outlet /> +</div> +<div *ngIf="modalService.getModalBehaviorSubject() | async" class="modal"> + <ng-container [ngTemplateOutlet]="modalService.getModalBehaviorSubject() | async" /> +</div> diff --git a/ufund-ui/src/app/app.component.ts b/ufund-ui/src/app/app.component.ts index 2fecc2d..78fd050 100644 --- a/ufund-ui/src/app/app.component.ts +++ b/ufund-ui/src/app/app.component.ts @@ -5,6 +5,7 @@ import {AuthService} from './services/auth.service'; import {ToastsService} from './services/toasts.service'; import {User, userType} from './models/User'; import {ActivatedRoute, Router} from '@angular/router'; +import {ModalService} from './services/modal.service'; @Component({ selector: 'app-root', @@ -22,6 +23,7 @@ export class AppComponent implements OnInit { private route: ActivatedRoute, protected toastService: ToastsService, private viewContainerRef: ViewContainerRef, + protected modalService: ModalService, @Inject(DOCUMENT) private document: Document ) {} diff --git a/ufund-ui/src/app/components/cupboard/cupboard.component.css b/ufund-ui/src/app/components/cupboard/cupboard.component.css index e45d929..9c37582 100644 --- a/ufund-ui/src/app/components/cupboard/cupboard.component.css +++ b/ufund-ui/src/app/components/cupboard/cupboard.component.css @@ -52,3 +52,36 @@ margin-top: 3px; } } + +#header2 { + display: flex; + flex-direction: column; + gap: 10px +} + +#searchArea { + display: flex; + + form { + display: flex; + width: 100%; + gap: 10px; + } + + input[type=text] { + display: flex; + width: 100%; + } +} + +#sortArea { + display: flex; + flex-direction: row; + gap: 10px; + align-items: center; +} + +select { + font-size: 14pt; + padding: 5px; +} diff --git a/ufund-ui/src/app/components/cupboard/cupboard.component.html b/ufund-ui/src/app/components/cupboard/cupboard.component.html index 37954bb..4eebc2d 100644 --- a/ufund-ui/src/app/components/cupboard/cupboard.component.html +++ b/ufund-ui/src/app/components/cupboard/cupboard.component.html @@ -1,38 +1,50 @@ <div id="box"> <div id="header"> <h1> Cupboard </h1> - <button *ngIf="isManager()" class="button2" (click)="this.selectForm('create')"><span class="icon">add</span>Create Need</button> + <ng-template #create> + <app-need-edit [mode]="'Create'" (refreshNeedList)="refresh()"></app-need-edit> + </ng-template> + <button *ngIf="usersService.isManager()" class="button2" (click)="modalService.showModal(create)"><span class="icon">add</span>Create Need</button> </div> - <app-need-list (currentNeed) = populateForm($event) #needList></app-need-list> -</div> -<ng-template [ngIf]="isManager()" > -<div> - <app-need-edit *ngIf="selectedForm === 'update'" [selectedNeed]="selectedNeed" (refreshNeedList)="needList.refresh()"></app-need-edit> - <div> - <div id="create-form" *ngIf="selectedForm === 'create'"> - <h1> Create Need </h1> - <form #cupboardForm="ngForm" (ngSubmit)="submit(cupboardForm.value)"> - <label>Name:</label><br> - <input type="text" name="name" ngModel><br> - <label>Image:</label><br> - <input type="text" name="image" ngModel><br> - <label>Location:</label><br> - <input type="text" name="location" ngModel><br> - <label>Max Goal:</label><br> - <input type="number" name="maxGoal" ngModel><br> - <label>Type</label><br> - <input type="radio" name="type" value="MONETARY" ngModel> - <label>Monetary</label><br> - <input type="radio" name="type" value="PHYSICAL" ngModel> - <label>Physical</label><br> - <input type="checkbox" name="urgent" value="false" ngModel> - <label>Urgent</label><br> - <label>Description</label><br> - <textarea name="description"></textarea><br> - <input type="submit" value="Submit"> - </form> - </div> + <div id="header2"> + <div id="searchArea"> + <form id="search-form" #searchForm="ngForm"> + <input type="text" name="search" class="wide-input" placeholder="Search in {{needs.length}} needs..." (input)="search(searchForm.value)" ngModel> + <input type="reset" value="Clear" (click)="search(null)"> <br> + </form> + </div> + <div id="sortArea"> + <label for="sort">Sort by: </label> + <select id='sort' [(ngModel)] = "currentSortAlgo" class="wide-input" (change)="search(searchForm.value)" [value]="currentSortAlgo"> + <option *ngFor="let algorithm of Object.keys(SortingAlgorithms)" [value]="algorithm"> + {{SortingAlgorithms[algorithm].display[sortMode === 'Ascending' ? 0 : 1]}} + </option> + </select> + <button (click)="toggleSortMode(searchForm.value)" [title]="sortMode"> + <span class="icon">{{sortMode === 'Ascending' ? 'arrow_upward': 'arrow_downward'}}</span> + </button> + <label>Needs per page: </label> + <input type ="number" [(ngModel)]="itemsPerPage" (change)="editItemsPerPage()" min="1" max="{{searchResults.length}}"> + </div> </div> + <h2 *ngIf="searchResults.length < needs.length && searchResults.length != 0"> Search Results({{needs.length - searchResults.length}} needs filtered): </h2> + <h2 *ngIf="searchResults.length == needs.length"> All Needs </h2> + <h2 *ngIf="searchResults.length == 0"> No Results Found </h2> + + <ng-template let-need #NLActions> + <button *ngIf="usersService.isHelper()" (click)="addToBasket(need)" [disabled]="usersService.inBasket(usersService.getBasket() | async, need)"> + <span class="icon">{{usersService.inBasket(usersService.getBasket() | async, need)? "check": "add" }}</span>Add To Basket + </button> + <ng-template #edit> + <app-need-edit *ngIf="need" [mode]="'Edit'" [need]="need" (refreshNeedList)="refresh()"></app-need-edit> + </ng-template> + <button *ngIf="usersService.isManager()" (click)="modalService.showModal(edit)"> + <span class="icon">edit</span>Edit Need + </button> + <button *ngIf="usersService.isManager()" (click)="deleteNeed(need.id)" > + <span class="icon">delete</span>Delete Need + </button> + </ng-template> + <app-need-list [uid]="0" [actionArea]="NLActions" *ngIf="searchResults.length > 0" [needs]="searchResults" [itemsPerPage]="itemsPerPage" #needList/> </div> -</ng-template> diff --git a/ufund-ui/src/app/components/cupboard/cupboard.component.ts b/ufund-ui/src/app/components/cupboard/cupboard.component.ts index a4706b3..f571566 100644 --- a/ufund-ui/src/app/components/cupboard/cupboard.component.ts +++ b/ufund-ui/src/app/components/cupboard/cupboard.component.ts @@ -1,11 +1,14 @@ import {Component, OnInit, ViewChild} from '@angular/core'; -import { CupboardService } from '../../services/cupboard.service'; -import { Need } from '../../models/Need'; -import { userType } from '../../models/User'; -import { catchError, of } from 'rxjs'; -import { NeedListComponent } from '../need-list/need-list.component'; +import {CupboardService} from '../../services/cupboard.service'; +import {Need} from '../../models/Need'; +import {catchError, of} from 'rxjs'; +import {NeedListComponent} from '../need-list/need-list.component'; import {AuthService} from '../../services/auth.service'; import {ToastsService, ToastType} from '../../services/toasts.service'; +import {UsersService} from '../../services/users.service'; +import {SortingAlgoArrays} from './sorting'; +import {Router} from '@angular/router'; +import {ModalService} from '../../services/modal.service'; @Component({ selector: 'app-cupboard', @@ -13,111 +16,139 @@ import {ToastsService, ToastType} from '../../services/toasts.service'; templateUrl: './cupboard.component.html', styleUrl: './cupboard.component.css' }) - export class CupboardComponent implements OnInit { - selectedForm?: string = undefined; - needs: any; + // selectedForm?: string = undefined; + // needs: any; @ViewChild("needList") needList?: NeedListComponent + private searchDelay: any; + needs: Need[] = []; + searchResults: Need[] = []; + sortMode: 'Ascending' | 'Descending' = 'Ascending' + itemsPerPage = parseInt(localStorage.getItem('itemsPerPage') ?? '5') ?? 5; + currentSortAlgo = 'sortByPriority'; + constructor( private cupboardService: CupboardService, private authService: AuthService, - private toastService: ToastsService + private toastService: ToastsService, + protected usersService: UsersService, + private router: Router, + protected modalService: ModalService ) {} ngOnInit(): void { - this.cupboardService.getNeeds().subscribe(n => this.needs = n); - if (this.isManager()) { - console.log("Admin view of Cupboard"); - } else { - console.log("Limited helper view of Cupboard"); - } + this.cupboardService.getNeeds().subscribe(n => { + this.needs = n; + // this.refresh() + this.search(null) + }); + this.authService.getCurrentUserSubject().subscribe( + () => this.usersService.refreshBasket()) } - selectedNeed: any = { - name: '', - location:'', - id: null, - maxGoal: null, - type: '', - urgent: false - }; - selectedNeedId: number | null = null; - searchResults: any[] = []; - - selectForm(name: string) { - //get search results from the need list - if (this.needList) { - this.searchResults = this.needList.searchResults; - } - console.log(this.searchResults) - this.selectedForm = name; - if (name == 'update') { - if (this.searchResults) { - this.searchResults.forEach((element: any) => { - console.log(element) - }); + refresh() { + this.cupboardService.getNeeds().subscribe(n => { + if (this.sortMode == 'Ascending') { + this.needs = n.sort(SortingAlgoArrays[this.currentSortAlgo].func); + } else { + this.needs = n.sort(SortingAlgoArrays[this.currentSortAlgo].func).reverse(); } + this.searchResults = this.needs; + // this.updateVisibleNeeds(); + }); - } + const form = document.getElementById('search-form') as HTMLFormElement; + form.reset(); + this.search(null); } - async updateSearchResults() { - if (this.needList) { - while (this.selectedForm == 'update') { - this.searchResults = this.needList.searchResults - await new Promise(resolve => setTimeout(resolve, 100)); - } + async search(form: any) { + console.log(this.currentSortAlgo) + //wait .25 seconds before searching but cancel if another search is made during the wait to prevent too many api calls + + //remove previous search if it exists + if (this.searchDelay) { + clearTimeout(this.searchDelay); } - } + if (form) { + this.searchDelay = setTimeout(() => { + + if (form) { - populateForm(need: any): void { - this.selectForm('update'); - this.selectedNeed = { ...need }; + const currentSearchValue = form.search; //latest value of the search + this.cupboardService.searchNeeds(currentSearchValue).subscribe((n) => { + if (this.sortMode == 'Ascending') { + this.searchResults = n.sort(SortingAlgoArrays[this.currentSortAlgo].func); + } else { + this.searchResults = n.sort(SortingAlgoArrays[this.currentSortAlgo].func).reverse(); + } + // this.updateVisibleNeeds(); + }); + } + }, 250); + } else { + //user has cleared the search bar, we can skip the timeout for a 1/4 second faster response + //clear timeout to stop pending search + clearTimeout(this.searchDelay); + this.searchResults = this.needs; + } } - isManager() { - const type = this.authService.getCurrentUser()?.type; - return type === ("MANAGER" as unknown as userType); + toggleSortMode(form : any) { + if (this.sortMode == 'Ascending'){ + this.sortMode = 'Descending' + } else { + this.sortMode = 'Ascending' + } + this.search(form) } - submit(form: any) { - const need: Need = { - name: form.name, - image: form.image, - location: form.location, - id: 0, - maxGoal: form.maxGoal, - type: form.type, - urgent: !!form.urgent, - filterAttributes: [], - current: 0, - description: form.description - }; - console.log("need:", need); - console.log("form submitted. creating need: ", need); - this.cupboardService.createNeed(need) + deleteNeed(id : number) { + this.cupboardService.deleteNeed(id) .pipe(catchError((ex, _) => { - if (ex.status == 500) { - this.toastService.sendToast(ToastType.ERROR, "Fields cannot be blank"); - } else if (ex.status == 400) { - this.toastService.sendToast(ToastType.ERROR, ex.error); - } else { - this.toastService.sendToast(ToastType.ERROR, "Error on creating need"); - } + this.toastService.sendToast(ToastType.ERROR, ex.error) return of() })) - .subscribe( - (result) => { - if (result) { - console.log("need created successfully"); - this.needList?.refresh() - } else { - console.log("need creation failed"); - } - } + .subscribe(() => { + this.toastService.sendToast(ToastType.INFO, "Need deleted.") + this.refresh(); + }) + } + + addToBasket(need: Need) { + const currentUser = this.authService.getCurrentUser(); + if (currentUser) { + if (!currentUser.basket.includes(need.id)) { + currentUser.basket.push(need.id); + this.usersService.updateUser(currentUser) + .pipe(catchError((err, _) => { + console.error(err); + return of(); + })) + .subscribe(() => { + let action = {label: "View Basket", onAction: () => this.router.navigate(['/basket'])} + this.toastService.sendToast(ToastType.INFO, `"${need.name}" Added to basket`, action) + this.usersService.refreshBasket(); + }); + } else { + this.toastService.sendToast(ToastType.ERROR, "This need is already in your basket!") + } + } + } - ); + editItemsPerPage() { + if (this.itemsPerPage > this.searchResults.length) { + this.itemsPerPage = this.searchResults.length + } + if (this.itemsPerPage < 1) { + this.itemsPerPage = 1 + } + localStorage.setItem('itemsPerPage', this.itemsPerPage.toString()) + this.refresh(); } + + protected readonly SortingAlgorithms = SortingAlgoArrays; + protected readonly Object = Object; } diff --git a/ufund-ui/src/app/components/cupboard/sorting.ts b/ufund-ui/src/app/components/cupboard/sorting.ts new file mode 100644 index 0000000..5c37019 --- /dev/null +++ b/ufund-ui/src/app/components/cupboard/sorting.ts @@ -0,0 +1,69 @@ +import {Need} from '../../models/Need'; + +interface sortAlgo { + (a: Need, b: Need): number; +} + +// sort functions +const sortByName: sortAlgo = (a: Need, b: Need): number => { + if(a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { + return -1; + } + return 1; +} + +const sortByGoal: sortAlgo = (a: Need, b: Need): number => { + if(a.maxGoal == b.maxGoal) { + return sortByName(a,b); + } + else if(a.maxGoal > b.maxGoal) { + return -1; + } + return 1; +} + +const sortByCompletion: sortAlgo = (a: Need, b: Need): number => { + if(a.current == b.current) { + return sortByGoal(a,b); + } + else if(a.current > b.current) { + return -1; + } + return 1; +} + +const sortByPriority: sortAlgo = (a: Need, b: Need): number => { + if(a.urgent == b.urgent) { + return sortByGoal(a,b); + } + else if(a.urgent && !b.urgent) { + return -1; + } + return 1; +} + +const sortByLocation: sortAlgo = (a: Need, b: Need): number => { + if(a.location.toLocaleLowerCase() < b.location.toLocaleLowerCase()) { + return -1; + } + return 1; +} + +const sortByType: sortAlgo = (a:Need, b:Need): number => { + if(a.type == b.type) { + return sortByName(a,b); + } + else if(a.type > b.type) { + return -1; + } + return 1; +} + +export const SortingAlgoArrays: {[key: string]: { func: sortAlgo, display: [string, string]}} = { + sortByPriority: { func: sortByPriority, display: ["Highest Priority", "Lowest Priority" ] }, + sortByName: { func: sortByName, display: ["Name (A to Z)", "Name (Z to A)" ] }, + sortByLocation: { func: sortByLocation, display: ["Location (A to Z)", "Location (Z to A)" ] }, + sortByCompletion: { func: sortByCompletion, display: ["Most Completed", "Least Completed" ] }, + sortByGoal: { func: sortByGoal, display: ["Largest Maximum Goal", "Smallest Maximum Goal" ] }, + sortByType: { func: sortByType, display: ["Type (Physical first)", "Type (Monetary first)" ] }, +}; diff --git a/ufund-ui/src/app/components/dashboard/dashboard.component.css b/ufund-ui/src/app/components/dashboard/dashboard.component.css index 185fdc2..54f362b 100644 --- a/ufund-ui/src/app/components/dashboard/dashboard.component.css +++ b/ufund-ui/src/app/components/dashboard/dashboard.component.css @@ -1,7 +1,45 @@ :host { display: flex; - flex-direction: column; + justify-content: center; +} + +#box { + display: flex; width: 800px; - align-self: center; - gap: 20px + flex-direction: column; + gap: 10px; +} + +#stats { + display: flex; + flex-direction: row; + gap: 10px; +} + +.card { + background-color: #2e2e2e; + width: 400px; + height: 130px; + border-radius: 5px; + padding: 10px; + display: flex; + flex-direction: column; + justify-content: space-between; + + h1 { + padding: 0 10px; + } +} + +.listCard { + display: flex; + flex-direction: column; + background-color: #2e2e2e; + border-radius: 5px; + padding: 10px; + gap: 10px; +} + +.content { + align-self: end; } diff --git a/ufund-ui/src/app/components/dashboard/dashboard.component.html b/ufund-ui/src/app/components/dashboard/dashboard.component.html index 2d7b4c3..233096a 100644 --- a/ufund-ui/src/app/components/dashboard/dashboard.component.html +++ b/ufund-ui/src/app/components/dashboard/dashboard.component.html @@ -1,10 +1,29 @@ +<div id="box"> + @if ((authService.getCurrentUserSubject() | async)?.type === userType.MANAGER) { + <h1>Admin Dashboard</h1> + <div id="stats"> + <div class="card"> + <span>Registered users</span> + <h1 class="content" *ngIf="count"> {{count | async}} </h1> + </div> -<h1>Admin Dashboard</h1> -<!--<app-mini-need-list [needList]="topNeeds" jtitle="Top needs" url="/cupboard"/>--> -<!--<app-mini-need-list [needList]="almostThere" jtitle="Almost there" url="/cupboard"/>--> -<!--<app-mini-need-list [needList]="inBasket" jtitle="In your basket" url="/basket"/>--> -<span>_ Registered users</span> -<span>_ Needs with overflow</span> -<span>_ Needs in peoples baskets</span> -<span>_ Total monetary contributions</span> -<span>_ </span> + <div class="card"> + <span>Total monetary contributions</span> + <h1 class="content" *ngIf="totalDonations"> ${{totalDonations | async}} </h1> + </div> + </div> + + <div class="listCard"> + <span>Fulfilled needs</span> + <app-mini-need-list [needList]="fulfilledNeeds.getValue()" label="Fulfilled needs"> </app-mini-need-list> + </div> + <div class="listCard"> + <span>Most fulfilled needs</span> + <app-mini-need-list [needList]="mostFulfilledNeeds.getValue()" label="Most fulfilled"> </app-mini-need-list> + </div> + + } @else { + <h1>Unauthorized</h1> + <span>This page requires you to be logged in as an admin! <a routerLink="/login">Log In</a></span> + } +</div> diff --git a/ufund-ui/src/app/components/dashboard/dashboard.component.ts b/ufund-ui/src/app/components/dashboard/dashboard.component.ts index c94b5c6..2ab4db2 100644 --- a/ufund-ui/src/app/components/dashboard/dashboard.component.ts +++ b/ufund-ui/src/app/components/dashboard/dashboard.component.ts @@ -1,10 +1,11 @@ import {Component, OnInit} from '@angular/core'; import {AuthService} from '../../services/auth.service'; import {Router} from '@angular/router'; -import {Need} from '../../models/Need'; import {CupboardService} from '../../services/cupboard.service'; -import {firstValueFrom} from 'rxjs'; import {UsersService} from '../../services/users.service'; +import {BehaviorSubject} from 'rxjs'; +import {GoalType, Need} from '../../models/Need'; +import {userType} from '../../models/User'; @Component({ selector: 'app-dashboard', @@ -14,9 +15,11 @@ import {UsersService} from '../../services/users.service'; }) export class DashboardComponent implements OnInit{ - topNeeds?: Need[] - almostThere?: Need[] - inBasket?: Need[] + protected count = new BehaviorSubject<number | undefined>(undefined) + protected totalDonations = new BehaviorSubject<number | undefined>(undefined) + protected totalNeeds = new BehaviorSubject<number | undefined>(undefined) + protected fulfilledNeeds = new BehaviorSubject<Need[] | undefined>(undefined) + protected mostFulfilledNeeds = new BehaviorSubject<Need[] | undefined>(undefined) constructor( protected authService: AuthService, @@ -26,20 +29,26 @@ export class DashboardComponent implements OnInit{ ) {} ngOnInit() { - let user = this.authService.getCurrentUser() - if(!localStorage.getItem("credential") && !user) { - this.router.navigate(['/login']) - return - } - - firstValueFrom(this.cupboardService.getNeeds()).then(r => { - this.topNeeds = r.sort((a, b) => b.current - a.current) - this.almostThere = r.sort((a, b) => a.current/a.maxGoal - b.current/b.maxGoal) - }) + this.userService.getCount().subscribe(count => this.count.next(count)) + this.cupboardService.getNeeds().subscribe(needs => { + let totalValue = 0 + for (let need of needs) { + if (need.type === GoalType.MONETARY) { + totalValue += need.current + this.totalDonations.next(totalValue) + } + + } + this.fulfilledNeeds.next(needs.filter(a => ((a.current / a.maxGoal)) >= 1)) + needs.sort((a, b) => b.current/b.maxGoal - a.current/a.maxGoal) - this.userService.getBasket().subscribe(r => { - this.inBasket = r; + needs = needs.filter(a => a.current != 0) + this.totalNeeds.next(needs.length) + this.mostFulfilledNeeds.next(needs.slice(0, 5)) }) + + } + protected readonly userType = userType; } diff --git a/ufund-ui/src/app/components/funding-basket/funding-basket.component.css b/ufund-ui/src/app/components/funding-basket/funding-basket.component.css index bd41fda..4764b0f 100644 --- a/ufund-ui/src/app/components/funding-basket/funding-basket.component.css +++ b/ufund-ui/src/app/components/funding-basket/funding-basket.component.css @@ -80,3 +80,11 @@ padding: 5px; gap: 5px; } + +#footer { + display: flex; + flex-direction: row; + align-items: center; + gap: 20px; + margin-bottom: 10px; +} diff --git a/ufund-ui/src/app/components/funding-basket/funding-basket.component.html b/ufund-ui/src/app/components/funding-basket/funding-basket.component.html index b88ef31..7158194 100644 --- a/ufund-ui/src/app/components/funding-basket/funding-basket.component.html +++ b/ufund-ui/src/app/components/funding-basket/funding-basket.component.html @@ -1,81 +1,25 @@ - -<!--<div id="needCount">--> -<!-- <label for="needCount">Needs in Basket:</label>--> -<!-- <span>{{ this.usersService.getBasket().getValue().length }}</span>--> -<!--</div>--> - -<!--<div *ngIf="this.usersService.getBasket().getValue().length == 0">--> -<!-- <h2>There are no needs in the basket</h2>--> -<!--</div>--> - -<!--<table class="needs" id="funding-basket" *ngIf="this.usersService.getBasket().getValue().length != 0">--> -<!-- <thead>--> -<!-- <tr>--> -<!-- <th class="need"></th>--> -<!-- </tr>--> -<!-- </thead>--> -<!-- <tbody>--> -<!-- <tr *ngFor="let need of usersService.getBasket().getValue()">--> -<!-- <td>--> -<!-- <a routerLink="/need/{{need.id}}">{{need.name}}</a>--> -<!-- <p>Goal: {{need.maxGoal}}</p>--> -<!-- <p>Current: {{(need.current).toFixed(2)}}</p>--> -<!-- <p>How much to Contribute: <input type="number" placeholder="insert value" min="1" id={{need.id}} class="contribution"></p>--> -<!-- <br>--> -<!-- <div>--> -<!-- <button type="button" class="removeNeed" title="delete need"--> -<!-- (click)="this.usersService.removeNeed(need.id)">Remove Need</button>--> -<!-- </div>--> -<!-- </td>--> -<!-- </tr>--> -<!-- </tbody>--> -<!--</table>--> -<!--<br>--> <div id="box"> - <h1>Funding Basket</h1> - <ng-template [ngIf]="usersService.getBasket().getValue().length"> - <div id="needList"> - <div *ngFor="let need of usersService.getBasket().getValue()" class="needEntry"> - <div [routerLink]="'/need/' + need.id" class="clickable"> - <div class="split"> - <div class="left"> - <span class="needName">{{need.name}}</span> - <span class="needType">{{need.type}}</span> - </div> - - <div class="right"> - <span *ngIf="need.urgent" class="urgent">URGENT</span> - <span *ngIf="need.location"><span class="icon">location_on</span>{{need.location}}</span> - </div> - </div> - - <br> - - <div class="prog"> - <span id="hover-status-label-{{need.id}}"> </span> - <span>{{need.current}}/{{need.maxGoal}} ({{((need.current / need.maxGoal) * 100).toFixed(0)}}%)</span> - <progress [value]="need.current" [max]="need.maxGoal"></progress> - </div> - - <!-- <div class="description">--> - <!-- {{need.description}}--> - <!-- </div>--> - </div> - - <div class="actionArea"> - <input type="number" placeholder="Quantity" min="1" id={{need.id}} class="contribution sort-scheme"> - <button class="removeNeed" title="delete need" (click)="this.usersService.removeNeed(need.id)"> - <span class="icon">delete</span> Remove from Basket - </button> - </div> + @if ((authService.getCurrentUserSubject() | async)?.type === userType.HELPER) { + <h1>Funding Basket</h1> + <ng-template [ngIf]="(usersService.getBasket() | async)?.length"> + <ng-template let-need #NLActions> + <input type="number" placeholder="Quantity" min="1" [id]="need?.id" class="contribution" (input)="resetColor($event)"> + <button class="removeNeed" (click)="this.usersService.removeNeed(need.id)"> + <span class="icon">delete</span>Remove from Basket + </button> + </ng-template> + <app-need-list [uid]="1" [actionArea]="NLActions" [needs]="(usersService.getBasket() | async)!"/> + <br> + <div id="footer"> + <button class="button2" title="checkout" (click)="checkout()">Checkout</button> + <span id="running-total">Your current running total is: ${{runningTotal | async}}</span> </div> + </ng-template> + <div *ngIf="!usersService.getBasket().getValue().length"> + <span>There are no needs in your basket! </span><a routerLink="/cupboard">Browse the cupboard</a> </div> - <br> - <div id="footer"> - <button class="button2" title="checkout" (click)="checkout()">Checkout</button> - </div> - </ng-template> - <div *ngIf="!usersService.getBasket().getValue().length"> - <span>There are no needs in your basket! </span><a routerLink="/cupboard">Browse the cupboard</a> - </div> + } @else { + <h1>Unauthorized</h1> + <span>This page requires you to be logged in as a user! <a routerLink="/login">Log In</a></span> + } </div> diff --git a/ufund-ui/src/app/components/funding-basket/funding-basket.component.ts b/ufund-ui/src/app/components/funding-basket/funding-basket.component.ts index 847baee..78ce958 100644 --- a/ufund-ui/src/app/components/funding-basket/funding-basket.component.ts +++ b/ufund-ui/src/app/components/funding-basket/funding-basket.component.ts @@ -2,9 +2,11 @@ import {Component, Input, OnInit, ViewChild} from '@angular/core'; import {UsersService} from '../../services/users.service'; import {Router} from '@angular/router'; import {CupboardService} from '../../services/cupboard.service'; -import {catchError, firstValueFrom, Observable} from 'rxjs'; +import {BehaviorSubject, firstValueFrom, of} from 'rxjs'; import {AuthService} from '../../services/auth.service'; import {ToastsService, ToastType} from '../../services/toasts.service'; +import {userType} from '../../models/User'; +import {GoalType} from '../../models/Need'; @Component({ selector: 'app-funding-basket', @@ -18,77 +20,61 @@ export class FundingBasketComponent implements OnInit { private router: Router, protected cupboardService: CupboardService, protected usersService: UsersService, - private authService: AuthService, + protected authService: AuthService, private toastService: ToastsService ) {} + public runningTotal = new BehaviorSubject(0) @ViewChild("contribution") contribution?: Input; - @Input() isValid: boolean = true; - // this is for login rerouting ngOnInit(): void { - if (!this.authService.getCurrentUser()) { - this.router.navigate(['/login'], {queryParams: {redir: this.router.url}}); - return; - } this.usersService.refreshBasket(); - // this.usersService.removeNeed(); <- call this to remove } async checkout() { - this.isValid = true; - for (let c of document.querySelectorAll('.contribution')!) { - let contribution = c as HTMLInputElement; - contribution.setAttribute("style", ""); - if (contribution.value == '' || contribution.valueAsNumber <= 0) { - this.isValid = false; + let order: { needID: number, quantity: number }[] = [] + let isNotValid = false + for (let contribution of document.querySelectorAll<HTMLInputElement>('.contribution')!) { + if (contribution.value == '' || contribution.valueAsNumber <= 0) { + isNotValid = true contribution.setAttribute("style", "border-color: #ff0000"); - this.toastService.sendToast(ToastType.ERROR, "Invalid input in funding basket!") - - setTimeout(() => { - contribution.setAttribute("style", "border-color: #ffffff"); - }, 3000); } + order.push({needID: +contribution.id, quantity: contribution.valueAsNumber}); } - // if (this.usersService.getBasket().value != await firstValueFrom(this.usersService.getUser(1)) - // for (let c of this.usersService.getBasket().value) { - // if (c == null) { - // this.isValid = false; - // this.statusText.next("One or more needs have been deleted") - // } else { - // this.statusText.next("test") - // } - // } - if (this.isValid) { - for (let c of document.querySelectorAll('.contribution')!) { - let contribution = c as HTMLInputElement; - let need = await firstValueFrom(this.cupboardService.getNeed(+contribution.id)); - need.current += +contribution.value; - this.usersService.removeNeed(+need.id); - this.cupboardService.checkoutNeed(need.id, +contribution.value) - .pipe(catchError((ex, _) => { - if (ex.status == 500) { - this.toastService.sendToast(ToastType.ERROR, 'Fields cannot be blank'); - } else if (ex.status == 400) { - this.toastService.sendToast(ToastType.ERROR, ex.error); - } else { - this.toastService.sendToast(ToastType.ERROR, 'Error on creating need'); - } - return new Observable<string>(); - })) - .subscribe((result) => { - if (result) { - //this.needList?.refresh() - } else { - console.log('need update failed'); - } - this.toastService.sendToast(ToastType.INFO, "Checkout successful"); - }); - } + + if (isNotValid) { + this.toastService.sendToast(ToastType.ERROR, "Invalid input in funding basket!") + return; + } + + try { + await firstValueFrom(this.cupboardService.checkoutNeed(order)) + } catch (ex:any) { + this.toastService.sendToast(ToastType.ERROR, ex.error); + return } + + order.forEach(contribution => this.usersService.removeNeed(contribution.needID)) + this.toastService.sendToast(ToastType.INFO, "Checkout successful"); } + resetColor(ev: any) { + let total = 0 + this.runningTotal.next(total); + for (let contribution of document.querySelectorAll<HTMLInputElement>('.contribution')!) { + this.cupboardService.getNeed(+contribution.id).subscribe(need => { + if (contribution.value != '' && need.type != GoalType.PHYSICAL) { + total += contribution.valueAsNumber + } + this.runningTotal.next(total); + }) + } + + (ev.target as HTMLInputElement).setAttribute("style", "border-color: unset") + } + protected readonly of = of; + protected readonly userType = userType; } diff --git a/ufund-ui/src/app/components/home-page/home-page.component.css b/ufund-ui/src/app/components/home-page/home-page.component.css index f6e5631..47e6ebd 100644 --- a/ufund-ui/src/app/components/home-page/home-page.component.css +++ b/ufund-ui/src/app/components/home-page/home-page.component.css @@ -1,5 +1,5 @@ :host { - height: 100%; + flex-grow: 1; display: flex; flex-direction: column; align-items: center; diff --git a/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.css b/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.css index ac456ab..6dceee1 100644 --- a/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.css +++ b/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.css @@ -1,29 +1,24 @@ :host { display: flex; flex-direction: column; - border: solid rgba(255, 255, 255, 0.5) 1px; border-radius: 5px; + height: 175px; } -#header { +#empty { + height: 175px; display: flex; - flex-direction: row; - justify-content: space-between; - border-bottom: solid rgba(255, 255, 255, 0.5) 1px; - padding: 10px; - - a { - display: flex; - } + align-items: center; + color: #878787; } #needList { display: flex; flex-direction: row; - padding: 10px; + /*padding: 10px;*/ gap: 10px; justify-content: start; - overflow: clip; + overflow: auto; } .needEntry { @@ -45,6 +40,10 @@ cursor: pointer; } +.needEntry:hover { + background-color: #444444; +} + .needName { font-weight: bold; } diff --git a/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.html b/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.html index a2de9e5..fc70c3d 100644 --- a/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.html +++ b/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.html @@ -1,9 +1,7 @@ -<div id="header"> - <span>{{jtitle}}</span> - <a [routerLink]="url">Show All<span class="icon">arrow_forward_ios</span></a> -</div> - <div id="needList"> + <div id="empty" *ngIf="!needList?.length"> + <span>(No needs)</span> + </div> <div class="needEntry" *ngFor="let need of needList" [routerLink]="'/need/'+need.id"> <div> <span class="needName">{{need.name}}</span> diff --git a/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.ts b/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.ts index c909ae6..18b176b 100644 --- a/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.ts +++ b/ufund-ui/src/app/components/mini-need-list/mini-need-list.component.ts @@ -10,7 +10,7 @@ import {Need} from '../../models/Need'; export class MiniNeedListComponent { @Input() needList?: Need[] - @Input() jtitle?: string + @Input() label?: string @Input() url?: string constructor( diff --git a/ufund-ui/src/app/components/need-edit/need-edit.component.css b/ufund-ui/src/app/components/need-edit/need-edit.component.css index 17605c2..b06e061 100644 --- a/ufund-ui/src/app/components/need-edit/need-edit.component.css +++ b/ufund-ui/src/app/components/need-edit/need-edit.component.css @@ -1,4 +1,4 @@ -:host { +#box { /*position: absolute;*/ /*background-color: rgba(0, 0, 0, 0.5);*/ /*display: flex;*/ @@ -8,14 +8,44 @@ /*right: 0;*/ /*z-index: 5;*/ /*justify-content: center;*/ + padding: 10px; + border-style: solid; + border-width: 1px; + border-color: #6c6c6c; + background-color: #2e2e2e; + border-radius: 5px; + position: relative; + width: 500px; } #create-form, #delete-form, #update-form { - margin-top: 50px; - background-color: #3a3a3a; - padding: 10px 20px 20px 20px; - border: 2px solid #000; - border-radius: 5px; + /*margin-top: 50px;*/ + + /*padding: 10px 20px 20px 20px;*/ + /*border: 2px solid #000;*/ + /*border-radius: 5px;*/ /*visibility: visible;*/ /*position: absolute;*/ + display: flex; + flex-direction: column; + gap: 10px; + + div { + display: flex; + flex-direction: column; + } +} + +#closeButton { + position: absolute; + width: 35px; + right: 10px; +} + +textarea { + height: 200px; +} + +label { + padding: 3px; } diff --git a/ufund-ui/src/app/components/need-edit/need-edit.component.html b/ufund-ui/src/app/components/need-edit/need-edit.component.html index e776415..ed4bfb3 100644 --- a/ufund-ui/src/app/components/need-edit/need-edit.component.html +++ b/ufund-ui/src/app/components/need-edit/need-edit.component.html @@ -1,23 +1,45 @@ -<div id="update-form"> - <h1> Update Need </h1> - <form #updateForm="ngForm" (ngSubmit)="update(updateForm.value)"> - <label>Name:</label><br> - <input type="text" name="name" [(ngModel)]="selectedNeed.name"><br> - <label>Image:</label><br> - <input type="text" name="image" [(ngModel)]="selectedNeed.image"><br> - <label>Location:</label><br> - <input type="text" name="location" [(ngModel)]="selectedNeed.location"><br> - <label>Max Goal:</label><br> - <input type="number" name="maxGoal" [(ngModel)]="selectedNeed.maxGoal"><br> - <label>Type</label><br> - <input type="radio" name="type" value="MONETARY" [(ngModel)]="selectedNeed.type"> - <label>Monetary</label><br> - <input type="radio" name="type" value="PHYSICAL" [(ngModel)]="selectedNeed.type"> - <label>Physical</label><br> - <input type="checkbox" name="urgent" [(ngModel)]="selectedNeed.urgent"> - <label>Urgent</label> <br> - <label>Description</label> <br> - <textarea name="description" [(ngModel)]="selectedNeed.description"></textarea><br> +<div id="box"> + <button id="closeButton" (click)="modalService.hideModal()"><span class="icon">close</span></button> + <h2>{{this.mode}} Need</h2> + <form #updateForm="ngForm" id="update-form" (ngSubmit)="submit(updateForm.value)"> + <div> + <span>Name:</span> + <input type="text" name="name" [(ngModel)]="needCopy.name"> + </div> + <div> + <span>Image:</span> + <input type="text" name="image" [(ngModel)]="needCopy.image"> + </div> + <div> + <span>Location:</span> + <input type="text" name="location" [(ngModel)]="needCopy.location"> + </div> + <div> + <span>Max Goal:</span> + <input type="number" name="maxGoal" [(ngModel)]="needCopy.maxGoal"> + </div> + <div> + <span>Type:</span> + <label> + <input type="radio" name="type" value="MONETARY" [(ngModel)]="needCopy.type"> + Monetary + </label> + <label> + <input type="radio" name="type" value="PHYSICAL" [(ngModel)]="needCopy.type"> + Physical + </label> + </div> + <div> + <span>Urgency:</span> + <label> + <input type="checkbox" name="urgent" [(ngModel)]="needCopy.urgent"> + Urgent + </label> + </div> + <div> + <span>Description:</span> + <textarea name="description" [(ngModel)]="needCopy.description"></textarea> + </div> <input type="submit" value="Submit"> </form> diff --git a/ufund-ui/src/app/components/need-edit/need-edit.component.ts b/ufund-ui/src/app/components/need-edit/need-edit.component.ts index 2462534..abfa543 100644 --- a/ufund-ui/src/app/components/need-edit/need-edit.component.ts +++ b/ufund-ui/src/app/components/need-edit/need-edit.component.ts @@ -1,61 +1,109 @@ -import { Component, Input, Output, EventEmitter } from '@angular/core'; -import { Need, GoalType } from '../../models/Need'; -import { CupboardService } from '../../services/cupboard.service'; -import { catchError, of } from 'rxjs'; -import { ToastsService, ToastType } from '../../services/toasts.service'; +import {Component, EventEmitter, Input, OnChanges, Output} from '@angular/core'; +import {GoalType, Need} from '../../models/Need'; +import {CupboardService} from '../../services/cupboard.service'; +import {catchError, of} from 'rxjs'; +import {ToastsService, ToastType} from '../../services/toasts.service'; +import {ModalService} from '../../services/modal.service'; +import {Router} from '@angular/router'; @Component({ - selector: 'app-need-edit', - standalone: false, - templateUrl: './need-edit.component.html', - styleUrl: './need-edit.component.css' + selector: 'app-need-edit', + standalone: false, + templateUrl: './need-edit.component.html', + styleUrl: './need-edit.component.css' }) -export class NeedEditComponent { - constructor( - private cupboardService: CupboardService, - private toastService: ToastsService - - ) {} - - @Input() selectedNeed!: Need; - @Output() refreshNeedList = new EventEmitter<void>(); - - update(form: any) { - console.log(form); - const need: Need = { - name: form.name, - image: form.image, - location: form.location, - id: this.selectedNeed.id, //system will control this - maxGoal: form.maxGoal, - type: GoalType[form.type as keyof typeof GoalType], - urgent: form.urgent, - filterAttributes: [], - current: 0, - description: form.description - }; - - this.cupboardService.updateNeed(need.id, need) - .pipe(catchError((ex, _) => { - if (ex.status == 500) { - this.toastService.sendToast(ToastType.ERROR, 'Fields cannot be blank'); - } else if (ex.status == 400) { - this.toastService.sendToast(ToastType.ERROR, ex.error); - } else { - this.toastService.sendToast(ToastType.ERROR, "Error on creating need"); - } - return of() - })) - .subscribe( - (result) => { - if (result) { - console.log("need updated successfully"); - this.refreshNeedList.emit(); +export class NeedEditComponent implements OnChanges { + + @Input() mode!: "Create" | "Edit" + @Input() need?: Need; + @Output() refreshNeedList = new EventEmitter<void>(); + + needCopy: any = {} + + constructor( + private cupboardService: CupboardService, + private toastService: ToastsService, + protected modalService: ModalService, + private router: Router + ) {} + + ngOnChanges() { + this.needCopy = {...this.need} + } + + submit(form: any) { + const need: Need = { + name: form.name, + image: form.image, + location: form.location, + id: this.needCopy.id, //system will control this + maxGoal: form.maxGoal, + type: GoalType[form.type as keyof typeof GoalType], + urgent: form.urgent, + filterAttributes: [], + current: 0, + description: form.description + }; + + if (this.mode == "Edit") { + this.updateNeed(need) + } else if (this.mode === 'Create') { + this.createNeed(need) + } + } + + updateNeed(need: Need) { + this.cupboardService.updateNeed(need.id, need) + .pipe(catchError((ex, _) => { + if (ex.status == 500) { + this.toastService.sendToast(ToastType.ERROR, 'Fields cannot be blank'); + } else if (ex.status == 400) { + this.toastService.sendToast(ToastType.ERROR, ex.error); + } else { + this.toastService.sendToast(ToastType.ERROR, "Error on creating need"); + } + return of() + })) + .subscribe( + (result) => { + if (result) { + let action = {label: 'View Need', onAction: () => this.router.navigate([`/need/${need.id}`])} + this.toastService.sendToast(ToastType.INFO, "Need updated successfully", action) + this.modalService.hideModal() + console.log("need updated successfully"); + this.refreshNeedList.emit(); + } else { + console.log("need update failed"); + } + } + ); + } + + createNeed(need: Need) { + this.cupboardService.createNeed(need) + .pipe(catchError((ex, _) => { + if (ex.status == 500) { + this.toastService.sendToast(ToastType.ERROR, "Fields cannot be blank"); + } else if (ex.status == 400) { + this.toastService.sendToast(ToastType.ERROR, ex.error); } else { - console.log("need update failed"); + this.toastService.sendToast(ToastType.ERROR, "Error on creating need"); + } + return of() + })) + .subscribe( + (result) => { + if (result) { + let action = {label: 'View Need', onAction: () => this.router.navigate([`/need/${result.id}`])} + this.toastService.sendToast(ToastType.INFO, "Need created successfully", action) + this.modalService.hideModal() + console.log("need created successfully"); + this.refreshNeedList.emit(); + } else { + console.log("need creation failed"); + } } - } - ); - } -}
\ No newline at end of file + ); + } +} diff --git a/ufund-ui/src/app/components/need-list/need-list.component.css b/ufund-ui/src/app/components/need-list/need-list.component.css index 56ae6a6..b3af85f 100644 --- a/ufund-ui/src/app/components/need-list/need-list.component.css +++ b/ufund-ui/src/app/components/need-list/need-list.component.css @@ -1,9 +1,3 @@ -#header { - display: flex; - flex-direction: column; - gap: 10px -} - .needEntry { background-color: var(--tertiary-color); display: flex; @@ -17,33 +11,6 @@ gap: 15px } -select { - font-size: 14pt; - padding: 5px; -} - -#searchArea { - display: flex; - - form { - display: flex; - width: 100%; - gap: 10px; - } - - input[type=text] { - display: flex; - width: 100%; - } -} - -#sortArea { - display: flex; - flex-direction: row; - gap: 10px; - align-items: center; -} - .needName { font-weight: bold; } @@ -53,18 +20,49 @@ select { font-size: 10pt; } +.need-image { + transition: all 0.15s ease-in-out; + height: 130px; + width: 200px; + margin: -10px 0 0 -10px; + object-fit: cover; + border-radius: 5px; + mask-image: linear-gradient(to right, rgb(255,255,255) 0, rgb(255,255,255,.1) 60%, rgb(255,255,255,0) 100%); +} + +/*.clickable:hover {*/ +/* .need-image {*/ +/* mask-image: none;*/ +/* width: 210px*/ +/* }*/ +/* .left {*/ +/* width: 32.5%;*/ +/* }*/ +/* .prog {*/ +/* width: 72.5%;*/ +/* }*/ +/*}*/ + .split { display: flex; flex-direction: row; - justify-content: space-between; - .left { + width: 15%; + transition: all 0.2s ease-in-out; + display: flex; + flex-direction: column; + } + + .middle { + width: 42.5%; display: flex; + align-items: start; flex-direction: column; } .right { + width: 42.5%; display: flex; flex-direction: column; align-items: end; @@ -80,8 +78,12 @@ select { } .prog { + transition: all 0.2s ease-in-out; + width: 85%; display: flex; flex-direction: column; + align-self: end; + margin-top: -5.25%; } .clickable { @@ -105,6 +107,10 @@ select { gap: 5px; } +.actionArea:empty { + padding: 0; +} + #page-selector { display: flex; align-items: center; diff --git a/ufund-ui/src/app/components/need-list/need-list.component.html b/ufund-ui/src/app/components/need-list/need-list.component.html index e9f70f6..0e5b762 100644 --- a/ufund-ui/src/app/components/need-list/need-list.component.html +++ b/ufund-ui/src/app/components/need-list/need-list.component.html @@ -1,39 +1,17 @@ -<div id="header"> - <div id="searchArea"> - <form id="search-form" #searchForm="ngForm"> - <input type="text" name="search" class="wide-input sort-scheme" placeholder="Search in {{needs.length}} needs..." (input)="search(searchForm.value)" ngModel> - <input type="reset" value="Clear" (click)="search(null)"> <br> - </form> - </div> - <div id="sortArea"> - <label for="sort">Sort by: </label> - <select [(ngModel)] = "sortSelection" class="wide-input sort-scheme" (change)="search(searchForm.value)" [value]="sortSelection"> - <option *ngFor="let algorithm of SortingAlgoArrays" value="{{algorithm.name}}"> - {{algorithm.display[sortMode === 'Ascending' ? 0 : 1]}} - </option> - </select> - <button (click)="changeSortMode(searchForm.value)"> - <span class="icon">{{sortMode === 'Ascending' ? 'arrow_upward': 'arrow_downward'}}</span> - </button> - <label>Needs per page: </label> - <input class="sort-scheme" type ="number" [(ngModel)]="itemsPerPage" (change)="editNeedsPerPage()" min="1" max="{{searchResults.length}}"> - </div> - <!--<button (click)="close()">Close</button>--> -</div> - <!-- display for when results are present and filtered--> -<h2 *ngIf="searchResults.length < needs.length && searchResults.length != 0"> Search Results({{needs.length - searchResults.length}} needs filtered): </h2> -<h2 *ngIf="searchResults.length == needs.length"> All Needs </h2> -<h2 *ngIf="searchResults.length == 0"> No Results Found </h2> <div id="needList"> <div *ngFor="let need of visibleNeeds" class="needEntry"> <div [routerLink]="'/need/' + need.id" class="clickable"> + <div class="split"> <div class="left"> + <img *ngIf="need.image" alt="Need image" class="need-image" [src]="need.image"/> + </div> + + <div class="middle"> <span class="needName">{{need.name}}</span> <span class="needType">{{need.type}}</span> </div> - <div class="right"> <span *ngIf="need.urgent" class="urgent">URGENT</span> <span *ngIf="need.location"><span class="icon">location_on</span>{{need.location}}</span> @@ -46,19 +24,10 @@ <span>{{need.type.toString() == 'MONETARY' ? '$' : ''}}{{need.current}}/{{need.type.toString() == 'MONETARY' ? '$' : ''}}{{need.maxGoal}} ({{((need.current / need.maxGoal) * 100).toFixed(0)}}%)</span> <progress [value]="need.current" [max]="need.maxGoal"></progress> </div> - </div> - <div class="actionArea"> - <button *ngIf="isHelper()" (click)="add(need)"> - <span class="icon">add</span>Add To Basket - </button> - <button *ngIf="isManager()" (click)="select(need)"> - <span class="icon">edit</span>Edit Need - </button> - <button *ngIf="isManager()" (click)="delete(need.id)" > - <span class="icon">delete</span>Delete Need - </button> + <div *ngIf="actionArea" class="actionArea"> + <ng-container [ngTemplateOutlet]="actionArea" [ngTemplateOutletContext]="{$implicit: need}"/> </div> </div> </div> diff --git a/ufund-ui/src/app/components/need-list/need-list.component.ts b/ufund-ui/src/app/components/need-list/need-list.component.ts index ae6bc99..7ca0ae7 100644 --- a/ufund-ui/src/app/components/need-list/need-list.component.ts +++ b/ufund-ui/src/app/components/need-list/need-list.component.ts @@ -1,293 +1,62 @@ -import {Component, EventEmitter, Output} from '@angular/core'; +import {Component, Input, OnChanges, TemplateRef} from '@angular/core'; import {GoalType, Need} from '../../models/Need'; -import {CupboardService} from '../../services/cupboard.service'; -import {UsersService} from '../../services/users.service'; -import {userType} from '../../models/User'; -import {AuthService} from '../../services/auth.service'; -import {catchError, of} from 'rxjs'; -import {ToastsService, ToastType} from '../../services/toasts.service'; - -interface sortAlgo { - (a: Need,b: Need): number; -} - -// sort functions -const sortByName: sortAlgo = (a: Need, b: Need): number => { - if(a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { - return -1; - } - return 1; -} - -const sortByGoal: sortAlgo = (a: Need, b: Need): number => { - if(a.maxGoal == b.maxGoal) { - return sortByName(a,b); - } - else if(a.maxGoal > b.maxGoal) { - return -1; - } - return 1; -} - -const sortByCompletion: sortAlgo = (a: Need, b: Need): number => { - if(a.current == b.current) { - return sortByGoal(a,b); - } - else if(a.current > b.current) { - return -1; - } - return 1; -} - -const sortByPriority: sortAlgo = (a: Need, b: Need): number => { - if(a.urgent == b.urgent) { - return sortByGoal(a,b); - } - else if(a.urgent && !b.urgent) { - return -1; - } - return 1; -} - -const sortByLocation: sortAlgo = (a: Need, b: Need): number => { - if(a.location.toLocaleLowerCase() < b.location.toLocaleLowerCase()) { - return -1; - } - return 1; -} @Component({ - selector: 'app-need-list', - standalone: false, - templateUrl: './need-list.component.html', - styleUrl: './need-list.component.css' + selector: 'app-need-list', + standalone: false, + templateUrl: './need-list.component.html', + styleUrl: './need-list.component.css' }) -export class NeedListComponent { - selectedNeed: Need | null = null; - needs: Need[] = []; - searchResults: Need[] = []; - visibleNeeds: Need[] = []; - sortMode: 'Ascending' | 'Descending' = 'Ascending' - currentPage: number = 0; - itemsPerPage: number = 5; - totalPages: number = Math.ceil(this.needs.length / this.itemsPerPage); - - getPrefix(need: Need) { - return (need.type === GoalType.MONETARY) ? "$" : ""; - } +export class NeedListComponent implements OnChanges { - decrementPage() { - this.currentPage--; - this.updateVisibleNeeds(); - } + @Input({required: true}) needs!: Need[] + @Input({required: true}) uid!: number + @Input() itemsPerPage: number = 5; + @Input() actionArea: TemplateRef<any> | null = null - incrementPage() { - this.currentPage++; - this.updateVisibleNeeds(); - } + visibleNeeds: Need[] = []; + currentPage: number = parseInt(localStorage.getItem('currentPage'+this.uid) ?? '0') ?? 0; + totalPages: number = 0; - lastPage() { - this.currentPage = this.totalPages - 1 - this.updateVisibleNeeds() - } - - firstPage() { - this.currentPage = 0 - this.updateVisibleNeeds() - } - - editNeedsPerPage() { - if (this.itemsPerPage > this.searchResults.length) { - this.itemsPerPage = this.searchResults.length; + ngOnChanges() { + this.updateVisibleNeeds() + this.currentPage = parseInt(localStorage.getItem('currentPage'+this.uid) ?? '0') ?? 0; } - if (this.itemsPerPage < 1) { - this.itemsPerPage = 1; - } - this.resetVisibleNeeds(); - } - - updateVisibleNeeds() { - this.totalPages = Math.ceil(this.searchResults.length / this.itemsPerPage); - this.visibleNeeds = this.searchResults.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage); - } - resetVisibleNeeds() { - this.currentPage = 0; - this.updateVisibleNeeds(); - } - - currentSortAlgo: sortAlgo = sortByPriority; - sortSelection: string = 'sortByPriority'; - - SortingAlgoArrays: {func:sortAlgo,name:string, display:string[]}[] = [ - {func:sortByPriority,name:"sortByPriority", display:["Highest Priority", "Lowest Priority"]}, - {func:sortByName,name:"sortByName", display:["Name (A to Z)", "Name (Z to A)"]}, - {func:sortByLocation,name:"sortByLocation", display:["Location (A to Z)", "Location (Z to A)"]}, - {func:sortByCompletion,name:"sortByCompletion", display:["Most Completed", "Least Completed"]}, - {func:sortByGoal,name:"sortByGoal", display:["Largest Maximum Goal", "Smallest Maximum Goal"]}, - ]; - - @Output() currentNeed = new EventEmitter<Need>(); - - constructor( - private cupboardService: CupboardService, - private usersService: UsersService, - private authService: AuthService, - private toastService: ToastsService - ) {} - - refresh() { - this.cupboardService.getNeeds().subscribe(n => { - if (this.sortMode == 'Ascending') { - this.needs = n.sort(this.currentSortAlgo); - } else { - this.needs = n.sort(this.currentSortAlgo).reverse(); - } - this.searchResults = this.needs; - this.updateVisibleNeeds(); - }); - - const form = document.getElementById('search-form') as HTMLFormElement; - form.reset(); - this.search(null); + getPrefix(need: Need) { + return (need.type === GoalType.MONETARY) ? "$" : ""; } - ngOnInit(): void { - this.refresh() - - } - - changeSortMode(form : any) { - if (this.sortMode == 'Ascending'){ - this.sortMode = 'Descending' - } else { - this.sortMode = 'Ascending' + //increment/decrement + decrementPage() { + this.currentPage--; + localStorage.setItem('currentPage'+this.uid, this.currentPage.toString()); + this.updateVisibleNeeds(); } - this.search(form) - } - private searchDelay: any; - - async search(form: any) { - //wait .25 seconds before searching but cancel if another search is made during the wait to prevent too many api calls - - //remove previous search if it exists - if (this.searchDelay) { - clearTimeout(this.searchDelay); + incrementPage() { + this.currentPage++; + localStorage.setItem('currentPage'+this.uid, this.currentPage.toString()); + this.updateVisibleNeeds(); } - if (form) { - this.searchDelay = setTimeout(() => { - - if (form) { - //sorting based on algo selected - this.SortingAlgoArrays.forEach(algo => { - if(algo.name === this.sortSelection) { - this.currentSortAlgo = algo.func; - console.log("changed sorting algorithm to: ", algo.name + this.sortMode) - return - } - }); - - const currentSearchValue = form.search; //latest value of the search - this.cupboardService.searchNeeds(currentSearchValue).subscribe((n) => { - if (this.sortMode == 'Ascending') { - this.searchResults = n.sort(this.currentSortAlgo); - } else { - this.searchResults = n.sort(this.currentSortAlgo).reverse(); - } - this.updateVisibleNeeds(); - }); - } - }, 250); - } else { - //user has cleared the search bar, we can skip the timeout for a 1/4 second faster response - //clear timeout to stop pending search - clearTimeout(this.searchDelay); - this.searchResults = this.needs; - } - } - delete(id : number) { - this.cupboardService.deleteNeed(id).subscribe(() => { - this.toastService.sendToast(ToastType.INFO, "Need deleted.") - this.needs = this.needs.filter(n => n.id !== id) - }) - this.refresh(); - } - - isManager() { - const type = this.authService.getCurrentUser()?.type; - return type === ("MANAGER" as unknown as userType); - } - - isHelper() { - const type = this.authService.getCurrentUser()?.type; - return type === ("HELPER" as unknown as userType); - } - - changeText(id : number, text : string) { - const span = document.getElementById('hover-status-label-' + id); - if (span) { - span.innerHTML = ' ' + text; + //skipping pages + lastPage() { + this.currentPage = this.totalPages - 1 + localStorage.setItem('currentPage'+this.uid, this.currentPage.toString()); + this.updateVisibleNeeds() } - } - add(need: Need) { - const currentUser = this.authService.getCurrentUser(); - //console.log("get current user in angular:", currentUser) - if (currentUser) { - if (!currentUser.basket.includes(need.id)) { - currentUser.basket.push(need.id); - this.toastService.sendToast(ToastType.INFO, "Need added to your basket!") - this.usersService.updateUser(currentUser) - .pipe(catchError((err, _) => { - console.error(err); - return of(); - })) - .subscribe(() => { - this.usersService.refreshBasket(); - }); - } else { - this.toastService.sendToast(ToastType.ERROR, "This need is already in your basket!") - } + firstPage() { + this.currentPage = 0 + localStorage.setItem('currentPage'+this.uid, this.currentPage.toString()); + this.updateVisibleNeeds() } - } - - back() { - this.searchResults = this.needs; - } - select(need : Need) { - //emit value - this.currentNeed.emit(need); - if (this.selectedNeed) { - //revert already selected need to previous style - console.log(need.id); - let button = document.getElementById('need-button-' + this.selectedNeed.id); - if (button) { - console.log(button) - button.style.background = 'lightgray'; - button.style.marginLeft = '0%'; - button.style.width = '98%'; - } - button = document.getElementById('need-edit-button-' + this.selectedNeed.id); - if (button) { - button.style.visibility = 'visible'; - } + updateVisibleNeeds() { + this.totalPages = Math.ceil(this.needs.length / this.itemsPerPage); + this.visibleNeeds = this.needs.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage); } - //change selected need to selected style - this.selectedNeed = need; - let button = document.getElementById('need-button-' + need.id); - if (button) { - button.style.background = 'white'; - button.style.marginLeft = '4%'; - button.style.width = '100%'; - } - button = document.getElementById('need-edit-button-' + need.id); - if (button) { - button.style.visibility = 'hidden'; - } - } protected readonly GoalType = GoalType; } diff --git a/ufund-ui/src/app/components/need-page/need-page.component.css b/ufund-ui/src/app/components/need-page/need-page.component.css index 47aa8b3..6ca1350 100644 --- a/ufund-ui/src/app/components/need-page/need-page.component.css +++ b/ufund-ui/src/app/components/need-page/need-page.component.css @@ -4,11 +4,13 @@ } #box { + /*padding-top: 7.5%;*/ display: flex; flex-direction: column; width: 800px; justify-content: start; gap: 10px; + padding: 0 10px; } .needName { @@ -22,32 +24,19 @@ /*margin-bottom: 20px;*/ } -.split { - display: flex; - flex-direction: row; - justify-content: space-between; - - - .left { - display: flex; - flex-direction: column; - width : 50%; - } - - .right { - display: flex; - flex-direction: column; - align-items: end; - } -} .need-image { - width: 400px; - height: auto; + width: calc(100% + 40px); + height: 40%; + /*position: absolute;*/ + left: 22.5%; aspect-ratio: 16/9; object-fit: cover; + mask-image: linear-gradient(to bottom, rgba(255,255,255,1) 0%, rgba(255,255,255,.2) 80%, rgba(255,255,255,.1) 90%, transparent 100%); border-radius: 10px; - box-shadow: var(--dark-highlight-clor) 0 0 50px; + margin-left: -20px; + margin-right: -20px; + margin-bottom: -80px; } .urgent { @@ -66,8 +55,8 @@ .actionArea { display: flex; - padding: 5px; - gap: 5px; + padding: 5px 0; + gap: 10px; margin-top: 10px; } diff --git a/ufund-ui/src/app/components/need-page/need-page.component.html b/ufund-ui/src/app/components/need-page/need-page.component.html index 6921eac..2629346 100644 --- a/ufund-ui/src/app/components/need-page/need-page.component.html +++ b/ufund-ui/src/app/components/need-page/need-page.component.html @@ -1,51 +1,50 @@ <div id="box"> - <h1>{{need.name}}</h1> - <span class="needType">{{need.type}} GOAL</span> - <p>{{need.description}}</p> - <div class="prog"> -<!-- <span>{{need?.current}} / {{need?.maxGoal}}</span>--> - <progress [value]="need.current" [max]="need.maxGoal"></progress> - <span>This goal is <strong>{{(((need.current)*100) / (need.maxGoal)).toFixed(0)}}%</strong> complete!</span> - </div> - + @if (need) { + <img *ngIf="need.image" alt="Need image" class="need-image" [src]="need.image"/> + <h1>{{need.name}}</h1> + <span class="needType">{{need.type}} GOAL</span> + <p>{{need.description}}</p> + <div class="prog"> + <progress [value]="need.current" [max]="need.maxGoal"></progress> + <span>This goal is <strong>{{(((need.current)*100) / (need.maxGoal)).toFixed(0)}}%</strong> complete!</span> + </div> - <div class="split"> - <div class="left"> <span><strong>Target Goal:</strong> {{(need.type === GoalType.MONETARY) ? "$" : ""}}{{need.maxGoal}}</span> <span><strong>Amount Currently Collected:</strong> {{need.type.toString() == 'MONETARY' ? '$' : ''}}{{need.current}}</span> <span><strong>Location:</strong> {{need.location}}</span> - <span><strong>Urgency: </strong> - <span *ngIf="!need.urgent">Not urgent</span> - <span *ngIf="need.urgent" class="urgent">URGENT</span> - </span> - - <div *ngIf="need.filterAttributes.length > 0"> - <strong>Tags:</strong> - <ul style="display: flex; column-gap: 24px;"> - <li *ngFor="let tag of need?.filterAttributes"> - <p>{{tag}}</p> - </li> - </ul> - </div> - </div> - <div class="right"> - <img *ngIf="need.image" alt="Need image" class="need-image" [src]="need.image"/> + <span><strong>Urgency: </strong> + <span *ngIf="!need.urgent">Not urgent</span> + <span *ngIf="need.urgent" class="urgent">URGENT</span> + </span> + + <div *ngIf="need.filterAttributes?.length"> + <strong>Tags:</strong> + <ul style="display: flex; column-gap: 24px;"> + <li *ngFor="let tag of need?.filterAttributes"> + <p>{{tag}}</p> + </li> + </ul> </div> - </div> - - <div class="actionArea"> - <button *ngIf="isHelper()" (click)="add(need!)"> - <span class="icon">add</span>Add To Basket - </button> -<!-- <button *ngIf="isManager()" (click)="edit(need!)">--> -<!-- <span class="icon">edit</span>Edit Need--> -<!-- </button>--> - <button *ngIf="isManager()" (click)="delete(need!.id)" > - <span class="icon">delete</span>Delete Need - </button> - </div> + <div class="actionArea"> + <button *ngIf="usersService.isHelper()" (click)="add(need)" [disabled]="usersService.inBasket(usersService.getBasket() | async, need)"> + <span class="icon">{{usersService.inBasket(usersService.getBasket() | async, need)? "check": "add" }}</span>Add To Basket + </button> + <ng-template #edit> + <app-need-edit [mode]="'Edit'" *ngIf="need" [need]="need" (refreshNeedList)="ngOnInit()"></app-need-edit> + </ng-template> + <button *ngIf="usersService.isManager()" (click)="modalService.showModal(edit)"> + <span class="icon">edit</span>Edit Need + </button> + <button *ngIf="usersService.isManager()" (click)="delete(need!.id)" > + <span class="icon">delete</span>Delete Need + </button> + </div> + } @else { + <h1>Need not found</h1> + <span>The requested need does not exist. <a routerLink="/cupboard">Browse the cupboard</a></span> + } </div> diff --git a/ufund-ui/src/app/components/need-page/need-page.component.ts b/ufund-ui/src/app/components/need-page/need-page.component.ts index ad4cacf..17e330c 100644 --- a/ufund-ui/src/app/components/need-page/need-page.component.ts +++ b/ufund-ui/src/app/components/need-page/need-page.component.ts @@ -1,12 +1,12 @@ -import {Component, Input} from '@angular/core'; +import {Component, Input, OnInit} from '@angular/core'; import {GoalType, Need} from '../../models/Need'; import {ActivatedRoute, Router} from "@angular/router"; import {CupboardService} from "../../services/cupboard.service"; -import {userType} from '../../models/User'; import {AuthService} from '../../services/auth.service'; import {catchError, of} from 'rxjs'; import {ToastsService, ToastType} from '../../services/toasts.service'; import {UsersService} from '../../services/users.service'; +import {ModalService} from '../../services/modal.service'; @Component({ selector: 'app-need-page', @@ -14,18 +14,17 @@ import {UsersService} from '../../services/users.service'; templateUrl: './need-page.component.html', styleUrl: './need-page.component.css' }) -export class NeedPageComponent { +export class NeedPageComponent implements OnInit { constructor( private route: ActivatedRoute, private cupboardService: CupboardService, private authService: AuthService, - private usersService: UsersService, + protected usersService: UsersService, private toastService: ToastsService, - private router: Router + private router: Router, + protected modalService: ModalService ) {} - public GoalType = GoalType; - @Input() need!: Need; ngOnInit(): void { @@ -33,29 +32,15 @@ export class NeedPageComponent { this.cupboardService.getNeed(id).subscribe(n => this.need = n); } - back() { - window.history.back(); - } - - isManager() { - const type = this.authService.getCurrentUser()?.type; - return type === ("MANAGER" as unknown as userType); - } - - isHelper() { - const type = this.authService.getCurrentUser()?.type; - return type === ("HELPER" as unknown as userType); - } - add(need: Need) { const currentUser = this.authService.getCurrentUser(); - //console.log("get current user in angular:", currentUser) if (currentUser) { if (!currentUser.basket.includes(need.id)) { currentUser.basket.push(need.id); this.usersService.updateUser(currentUser) .pipe(catchError((err, _) => { - console.error(err); + let action = {label: "View Basket", onAction: () => this.router.navigate(['/basket'])} + this.toastService.sendToast(ToastType.INFO, `"${need.name}" Added to basket`, action) return of(); })) .subscribe(() => { @@ -69,19 +54,16 @@ export class NeedPageComponent { delete(id : number) { this.cupboardService.deleteNeed(id) - .pipe(catchError((ex, r) => { + .pipe(catchError((ex, _) => { this.toastService.sendToast(ToastType.ERROR, ex.error) return of() })) .subscribe(() => { - // this.needs = this.needs.filter(n => n.id !== id) - this.toastService.sendToast(ToastType.INFO, "Need deleted") - this.router.navigate(['/']) + this.toastService.sendToast(ToastType.INFO, "Need deleted.") + this.router.navigate(['/cupboard']) }) // this.refresh(); } - edit(need: Need) { - - } + readonly GoalType = GoalType } diff --git a/ufund-ui/src/app/components/signup/signup.component.css b/ufund-ui/src/app/components/signup/signup.component.css index 429bc42..aa90e04 100644 --- a/ufund-ui/src/app/components/signup/signup.component.css +++ b/ufund-ui/src/app/components/signup/signup.component.css @@ -3,15 +3,26 @@ align-items: center; justify-content: center; height: 100%; - margin-top: -66px + margin-top: -66px; + background: rgba(0, 0, 0, .65) url("https://4kwallpapers.com/images/walls/thumbs_2t/13136.png"); + background-blend-mode: darken; + background-size: cover; } #box { display: flex; flex-direction: column; - /*max-width: 300px;*/ + max-width: 500px; gap: 10px; + backdrop-filter: blur(25px); + background-color: rgba(0, 0, 0, 0.1); + padding: 30px; + color: white; + border-radius: 5px; + border-style: solid; + border-width: 1px; + border-color: rgb(140, 140, 255); & > div { display: flex; @@ -19,6 +30,11 @@ } } +#password { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + .border { border-style: solid; border-width: 1px; @@ -34,6 +50,8 @@ width: 100%; appearance: none; overflow: hidden; + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; /*margin-top: -5px;*/ } @@ -57,13 +75,10 @@ #passReq { display: flex; flex-direction: column; + margin-top: 10px; } #box > div { - display: flex; - flex-direction: row; - align-items: start; - gap: 20px; div { display: flex; diff --git a/ufund-ui/src/app/components/signup/signup.component.html b/ufund-ui/src/app/components/signup/signup.component.html index 84f15e4..ef2fc27 100644 --- a/ufund-ui/src/app/components/signup/signup.component.html +++ b/ufund-ui/src/app/components/signup/signup.component.html @@ -7,13 +7,12 @@ <div> <div> - <input placeholder="Password" type="password" (input)="validate(username.value, confirmPass.value, password.value)" #password> + <input id="password" placeholder="Password" type="password" (input)="validate(username.value, confirmPass.value, password.value)" #password> <progress [ngClass]="'color' + strength.getValue()" id="bar" [value]="strength | async" max="5"> </progress> <span *ngIf="passwordStatusText">{{passwordStatusText | async}}</span> - </div> - - <div id="passReq"> - <span *ngFor="let requirement of Object.values(passwordRequirements)" [style.color]="requirement.value ? 'green' : 'red'"><span class="icon">{{requirement.value?"check":"close"}}</span> {{requirement.title}}</span> + <div id="passReq"> + <span *ngFor="let requirement of Object.values(passwordRequirements)" [style.color]="requirement.value ? 'green' : 'red'"><span class="icon">{{requirement.value?"check":"close"}}</span> {{requirement.title}}</span> + </div> </div> </div> diff --git a/ufund-ui/src/app/components/signup/signup.component.ts b/ufund-ui/src/app/components/signup/signup.component.ts index 9c37211..2762d03 100644 --- a/ufund-ui/src/app/components/signup/signup.component.ts +++ b/ufund-ui/src/app/components/signup/signup.component.ts @@ -1,4 +1,4 @@ -import {Component} from '@angular/core'; +import {Component, ElementRef, ViewChild} from '@angular/core'; import {UsersService} from '../../services/users.service'; import {Router} from '@angular/router'; import {BehaviorSubject} from 'rxjs'; @@ -29,6 +29,7 @@ export class SignupComponent { protected ableToCreateAccount = new BehaviorSubject(false) protected passwordRequirements: PasswordRequirements = new PasswordRequirements() protected strength = new BehaviorSubject(0) + @ViewChild("username") usernameInput!: ElementRef<HTMLInputElement> constructor( protected usersService: UsersService, @@ -56,11 +57,11 @@ export class SignupComponent { validate(username: string, passConfirm:string, password: string) { this.passwordsMatch.next(false) - this.usernameStatusText.next("") + this.usernameInput.nativeElement.setAttribute("style", "") this.checkPasswordStrength(password); if (username === "") { - this.usernameStatusText.next("Username field can't be blank") + this.usernameInput.nativeElement.setAttribute("style", "border-color: #ff0000") } if (passConfirm && password === passConfirm) { @@ -105,8 +106,6 @@ export class SignupComponent { this.passwordStatusText.next("") } else if (strength == 0) { this.passwordStatusText.next("") - } else { - this.passwordStatusText.next("Password must meet requirements") } this.strength.next(strength) diff --git a/ufund-ui/src/app/components/toast/toast.component.css b/ufund-ui/src/app/components/toast/toast.component.css index 4cd81fe..82e2ff3 100644 --- a/ufund-ui/src/app/components/toast/toast.component.css +++ b/ufund-ui/src/app/components/toast/toast.component.css @@ -14,7 +14,7 @@ animation: slideDown .5s ease-in-out; transition: transform .5s; align-self: center; - z-index: 3; + z-index: 4; position: absolute; top: 15px; display: flex; @@ -39,7 +39,7 @@ } } -.toast.hide { +.toast.hide:not(:has(:hover)) { transform: translateY(-90px); } diff --git a/ufund-ui/src/app/components/toast/toast.component.html b/ufund-ui/src/app/components/toast/toast.component.html index dccf869..dc33ecd 100644 --- a/ufund-ui/src/app/components/toast/toast.component.html +++ b/ufund-ui/src/app/components/toast/toast.component.html @@ -1,6 +1,6 @@ <div class="toast" [ngClass]="ToastType[type].toLowerCase()" #toastDiv> <span>{{this.message}}</span> - <a *ngIf="this.action" (click)="this.action.onAction()">{{this.action.label}}</a> + <a *ngIf="this.action" href="#" (click)="this.action.onAction(); $event.preventDefault(); hide()">{{this.action.label}}</a> <button (click)="hide()"> <span class="icon">close</span> </button> diff --git a/ufund-ui/src/app/components/toast/toast.component.ts b/ufund-ui/src/app/components/toast/toast.component.ts index 47fd7ff..6bbae34 100644 --- a/ufund-ui/src/app/components/toast/toast.component.ts +++ b/ufund-ui/src/app/components/toast/toast.component.ts @@ -21,7 +21,6 @@ export class ToastComponent implements OnInit{ } hide() { - console.log(this.toastDiv, typeof this.toastDiv) this.toastDiv.nativeElement.classList.add('hide') } diff --git a/ufund-ui/src/app/services/cupboard.service.ts b/ufund-ui/src/app/services/cupboard.service.ts index 9232c0c..a87dee2 100644 --- a/ufund-ui/src/app/services/cupboard.service.ts +++ b/ufund-ui/src/app/services/cupboard.service.ts @@ -23,8 +23,8 @@ export class CupboardService { private authService: AuthService ) {} - createNeed(need: Need): Observable<boolean> { - return this.http.post<boolean>(this.url, need, this.httpOptions()) + createNeed(need: Need): Observable<Need> { + return this.http.post<Need>(this.url, need, this.httpOptions()) } getNeeds(): Observable<Need[]> { @@ -47,7 +47,7 @@ export class CupboardService { return this.http.delete<boolean>(`${this.url}/${id}`, this.httpOptions()) } - checkoutNeed(id: number, quantity: number) { - return this.http.put(`${this.url}/checkout`, {needID: id, amount: quantity}, this.httpOptions()) + checkoutNeed(data: {needID: number, quantity: number}[]) { + return this.http.put(`${this.url}/checkout`, data, this.httpOptions()) } } diff --git a/ufund-ui/src/app/services/modal.service.ts b/ufund-ui/src/app/services/modal.service.ts new file mode 100644 index 0000000..04f2f3a --- /dev/null +++ b/ufund-ui/src/app/services/modal.service.ts @@ -0,0 +1,25 @@ +import {Injectable, TemplateRef} from '@angular/core'; +import {BehaviorSubject} from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class ModalService { + + private modal = new BehaviorSubject<TemplateRef<any> | null>(null) + + constructor() {} + + showModal(template: TemplateRef<any>) { + console.log("got here", template) + this.modal.next(template) + } + + hideModal() { + this.modal.next(null) + } + + getModalBehaviorSubject() { + return this.modal; + } +} diff --git a/ufund-ui/src/app/services/users.service.ts b/ufund-ui/src/app/services/users.service.ts index 4080ebf..688d6e4 100644 --- a/ufund-ui/src/app/services/users.service.ts +++ b/ufund-ui/src/app/services/users.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import {HttpClient, HttpHeaders} from '@angular/common/http'; import {BehaviorSubject, catchError, firstValueFrom, Observable, of} from 'rxjs'; -import {User} from '../models/User'; +import {User, userType} from '../models/User'; import { Need } from '../models/Need'; import { CupboardService } from './cupboard.service'; import {AuthService} from './auth.service'; @@ -21,11 +21,22 @@ export class UsersService { }) }); + httpOptions2 = () => ({ + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + "jelly-api-key": this.authService.getApiKey() + }), + responseType: "text" as "json" // don't ask me how or why this works, bc i have no clue... + // see the relevant angular bug report https://github.com/angular/angular/issues/18586 + }); + constructor( private http: HttpClient, private cupboardService: CupboardService, private authService: AuthService - ) {} + ) { + authService.getCurrentUserSubject().subscribe(() => this.refreshBasket()) + } async createUser(username:string, password:string) { await firstValueFrom(this.http.post<User>(this.url, {username: username, password: password}, this.httpOptions())) @@ -35,6 +46,10 @@ export class UsersService { return this.http.get<User>(`${this.url}/${id}`, this.httpOptions()) } + getCount(): Observable<number> { + return this.http.get<number>(`${this.url}/count`, this.httpOptions2()) + } + updateUser(user: User): Observable<User> { console.log(`${this.url}/${user.username}`, user, this.httpOptions) return this.http.put<User>(`${this.url}/${user.username}`, user, this.httpOptions()) @@ -45,7 +60,9 @@ export class UsersService { } refreshBasket() { - let promiseArr = this.authService.getCurrentUser()!.basket.map(async needID => { + let usr = this.authService.getCurrentUser(); + if (!usr) return; + let promiseArr = usr.basket.map(async needID => { return await firstValueFrom(this.cupboardService.getNeed(needID)); }) Promise.all(promiseArr).then(r => this.basket.next(r)); @@ -71,4 +88,16 @@ export class UsersService { return this.basket; } + isManager() { + return this.authService.getCurrentUser()?.type === userType.MANAGER + } + + isHelper() { + return this.authService.getCurrentUser()?.type === userType.HELPER + } + + inBasket(basket: Need[] | null, need: Need) { + return basket?.map(r => r.id).includes(need.id); + } + } diff --git a/ufund-ui/src/styles.css b/ufund-ui/src/styles.css index 09e354c..44f6107 100644 --- a/ufund-ui/src/styles.css +++ b/ufund-ui/src/styles.css @@ -46,7 +46,9 @@ body { font-optical-sizing: auto; } -input { +input, textarea { + resize: none; + font-family: Inter, sans-serif; font-size: 14pt; padding: 5px; border-radius: 5px; @@ -70,6 +72,7 @@ button, input[type=button], input[type=reset], input[type=submit], .button { gap: 5px; text-align: center; justify-content: center; + align-items: center; &:hover { background-color: var(--hover-color); |