From fde39fc28693286ae440714c020fad66e62ce64b Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 6 Apr 2026 22:46:14 +0200 Subject: [PATCH 01/10] Level and photo previews on user page --- src/app/api/client.service.ts | 4 + src/app/pages/user/user.component.html | 72 ++++++++++ src/app/pages/user/user.component.ts | 179 ++++++++++++++++++++++++- 3 files changed, 251 insertions(+), 4 deletions(-) diff --git a/src/app/api/client.service.ts b/src/app/api/client.service.ts index f217728..15b5669 100644 --- a/src/app/api/client.service.ts +++ b/src/app/api/client.service.ts @@ -147,6 +147,10 @@ export class ClientService extends ApiImplementation { return this.http.get>(`/photos`, {params: this.createPageQuery(skip, count)}); } + getPhotosRelatedToUserUuid(userId: string, category: string, skip: number = 0, count: number = defaultPageSize) { + return this.http.get>(`/photos/${category}/uuid/${userId}`, {params: this.createPageQuery(skip, count)}); + } + getContests() { return this.http.get>("/contests"); } diff --git a/src/app/pages/user/user.component.html b/src/app/pages/user/user.component.html index aceefd8..4028933 100644 --- a/src/app/pages/user/user.component.html +++ b/src/app/pages/user/user.component.html @@ -18,4 +18,76 @@ } + + + +
+ + + Levels + + + + + +
+ + +
+
+
+ + @if (currentLevels.length > 0) { + @for (level of currentLevels; track level.levelId; let i = $index) { +
+ + + +
+ } + } + @else { +

No levels yet...

+ } +
+ +
+ + + Photos + + + + + +
+ + +
+
+
+ + @if (currentPhotos.length > 0) { + @for (photo of currentPhotos; track photo.photoId; let i = $index) { +
+ +
+ } + + } + @else { +

No photos yet...

+ } +
+
} diff --git a/src/app/pages/user/user.component.ts b/src/app/pages/user/user.component.ts index 2498708..933be0a 100644 --- a/src/app/pages/user/user.component.ts +++ b/src/app/pages/user/user.component.ts @@ -1,8 +1,7 @@ import { Component } from '@angular/core'; import {User} from "../../api/types/users/user"; -import {TitleService} from "../../services/title.service"; import {ClientService} from "../../api/client.service"; -import {ActivatedRoute} from "@angular/router"; +import { ActivatedRoute, RouterLink } from "@angular/router"; import {DefaultPipe} from "../../pipes/default.pipe"; import { AsyncPipe } from "@angular/common"; import {UserAvatarComponent} from "../../components/ui/photos/user-avatar.component"; @@ -15,6 +14,22 @@ import { UserRelations } from '../../api/types/users/user-relations'; import { FancyHeaderUserButtonsComponent } from "../../components/ui/layouts/fancy-header-user-buttons.component"; import { ExtendedUser } from '../../api/types/users/extended-user'; import { AuthenticationService } from '../../api/authentication.service'; +import { TwoPaneLayoutComponent } from "../../components/ui/layouts/two-pane-layout.component"; +import { ContainerComponent } from "../../components/ui/container.component"; +import { PaneTitleComponent } from "../../components/ui/text/pane-title.component"; +import { DropdownMenuComponent } from "../../components/ui/form/dropdown-menu.component"; +import { ButtonComponent } from "../../components/ui/form/button.component"; +import { RadioButtonComponent } from "../../components/ui/form/radio-button.component"; +import { Level } from '../../api/types/levels/level'; +import { Photo } from '../../api/types/photos/photo'; +import { FormControl, FormGroup } from '@angular/forms'; +import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; +import { DividerComponent } from "../../components/ui/divider.component"; +import { BannerService } from '../../banners/banner.service'; +import { RefreshApiError } from '../../api/refresh-api-error'; +import { LevelPreviewComponent } from "../../components/items/level-preview.component"; +import { PhotoComponent } from "../../components/items/photo.component"; +import { DarkContainerComponent } from "../../components/ui/dark-container.component"; @Component({ selector: 'app-user', @@ -26,7 +41,18 @@ import { AuthenticationService } from '../../api/authentication.service'; AsyncPipe, UserStatusComponent, UserStatisticsComponent, - FancyHeaderUserButtonsComponent + FancyHeaderUserButtonsComponent, + TwoPaneLayoutComponent, + ContainerComponent, + PaneTitleComponent, + RouterLink, + DropdownMenuComponent, + ButtonComponent, + RadioButtonComponent, + DividerComponent, + LevelPreviewComponent, + PhotoComponent, + DarkContainerComponent ], templateUrl: './user.component.html', styles: `` @@ -37,7 +63,29 @@ export class UserComponent { protected ownUser: ExtendedUser | undefined; protected isMobile: boolean = false; - constructor(private auth: AuthenticationService, private client: ClientService, route: ActivatedRoute, protected layout: LayoutService) { + levelsPublishedByUser: Level[] | undefined; + levelsHeartedByUser: Level[] | undefined; + currentLevels: Level[] = []; + + showLevelDropdown: boolean = false; + levelSelectionString: string = ""; + levelForm = new FormGroup({ + selection: new FormControl(0) + }); + + photosByUser: Photo[] | undefined; + photosWithUser: Photo[] | undefined; + currentPhotos: Photo[] = []; + + showPhotoDropdown: boolean = false; + photoSelectionString: string = ""; + photoForm = new FormGroup({ + selection: new FormControl(0) + }); + + constructor(private auth: AuthenticationService, private client: ClientService, route: ActivatedRoute, protected layout: LayoutService, + protected banner: BannerService + ) { route.params.subscribe(params => { const username: string | undefined = params['username']; const uuid: string | undefined = params['uuid']; @@ -51,9 +99,132 @@ export class UserComponent { this.ownUser = user; } }); + + this.getLevels(0); + this.getPhotos(0); }); }); this.layout.isMobile.subscribe(v => this.isMobile = v); } + + levelSelectionButtonClick() { + this.showLevelDropdown = !this.showLevelDropdown; + } + + getLevels(selection: number) { + if (this.user == null) return; + let cachedList: Level[] | undefined; + + switch (selection) { + case 0: + this.levelSelectionString = "byUser"; + cachedList = this.levelsPublishedByUser; + break; + case 1: + this.levelSelectionString = "hearted"; + cachedList = this.levelsHeartedByUser; + break; + default: + this.banner.warn("Cannot get levels", "Selection" + selection + " is unknown"); + return; + } + this.levelForm.controls.selection.setValue(selection); + + if (cachedList != null) { + this.currentLevels = cachedList; + return; + } + + this.client.getLevelsInCategory(this.levelSelectionString, 0, 5, {u: this.user.username}).subscribe({ + error: error => { + const apiError: RefreshApiError | undefined = error.error?.error; + this.banner.warn("Failed to get levels", apiError == null ? error.message : apiError.message); + }, + next: levelPage => { + // cache the page + switch (selection) { + case 0: + this.levelsPublishedByUser = levelPage.data; + break; + case 1: + this.levelsHeartedByUser = levelPage.data; + break; + } + + this.currentLevels = levelPage.data; + } + }); + } + + getLevelSelectionText(selection: number): string { + switch (selection) { + case 0: return "Published by user"; + case 1: return "Hearted by user"; + default: "Something by user"; + } + return ""; + } + + photoSelectionButtonClick() { + this.showPhotoDropdown = !this.showPhotoDropdown; + } + + getPhotos(selection: number) { + if (this.user == null) return; + let cachedList: Photo[] | undefined; + + switch (selection) { + case 0: + this.photoSelectionString = "by"; + cachedList = this.photosByUser; + break; + case 1: + this.photoSelectionString = "with"; + cachedList = this.photosWithUser; + break; + default: + this.banner.warn("Cannot get photos", "Selection" + selection + " is unknown"); + return; + } + this.photoForm.controls.selection.setValue(selection); + + if (cachedList != null) { + this.currentPhotos = cachedList; + return; + } + + + this.client.getPhotosRelatedToUserUuid(this.user.userId, this.photoSelectionString, 0, 2).subscribe({ + error: error => { + const apiError: RefreshApiError | undefined = error.error?.error; + this.banner.warn("Failed to get photos with user", apiError == null ? error.message : apiError.message); + }, + next: photoPage => { + // cache the page + switch (selection) { + case 0: + this.photosByUser = photoPage.data; + break; + case 1: + this.photosWithUser = photoPage.data; + break; + } + + this.currentPhotos = photoPage.data; + } + }); + } + + getPhotoSelectionText(selection: number): string { + switch (selection) { + case 0: return "By user"; + case 1: return "With user"; + default: "Something user"; + } + return ""; + } + + protected readonly faChevronDown = faChevronDown; + protected readonly faChevronUp = faChevronUp; } From f77aca3d9255f727efdba12307ec0f860f23ed2a Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Wed, 8 Apr 2026 18:47:22 +0200 Subject: [PATCH 02/10] Proper pages for levels and photos by user --- src/app/app.routes.ts | 9 +- .../level-user-listing.component.html | 35 ++++ .../level-user-listing.component.ts | 167 ++++++++++++++++++ .../photo-user-listing.component.html | 35 ++++ .../photo-user-listing.component.ts | 167 ++++++++++++++++++ src/app/pages/user/user.component.html | 52 +++--- src/app/pages/user/user.component.ts | 4 +- 7 files changed, 439 insertions(+), 30 deletions(-) create mode 100644 src/app/pages/level-user-listing/level-user-listing.component.html create mode 100644 src/app/pages/level-user-listing/level-user-listing.component.ts create mode 100644 src/app/pages/photo-user-listing/photo-user-listing.component.html create mode 100644 src/app/pages/photo-user-listing/photo-user-listing.component.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index d0793d9..0b5c126 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -17,6 +17,11 @@ export const routes: Routes = [ loadComponent: () => import('./pages/level-listing/level-listing.component').then(x => x.LevelListingComponent), data: {title: "Level Listing"} }, + { + path: 'levels/:category/user/:username', + loadComponent: () => import('./pages/level-user-listing/level-user-listing.component').then(x => x.LevelUserListingComponent), + data: {title: "Level Listing"} + }, { path: 'level/:id/:slug', loadComponent: () => import('./pages/level/level.component').then(x => x.LevelComponent), @@ -46,8 +51,8 @@ export const routes: Routes = [ data: {title: "Photos"}, }, { - path: 'photos', - loadComponent: () => import('./pages/photo-listing/photo-listing.component').then(x => x.PhotoListingComponent), + path: 'photos/:category/user/:username', + loadComponent: () => import('./pages/photo-user-listing/photo-user-listing.component').then(x => x.PhotoUserListingComponent), data: {title: "Photos"}, }, { diff --git a/src/app/pages/level-user-listing/level-user-listing.component.html b/src/app/pages/level-user-listing/level-user-listing.component.html new file mode 100644 index 0000000..6bb022f --- /dev/null +++ b/src/app/pages/level-user-listing/level-user-listing.component.html @@ -0,0 +1,35 @@ +@if (user) { + +
+
+ + + + +
+ + +
+
+ +
+
+
+ + @if (currentLevels) { + + @for (level of this.currentLevels; track level.levelId) { + + + + } + + } + + +} diff --git a/src/app/pages/level-user-listing/level-user-listing.component.ts b/src/app/pages/level-user-listing/level-user-listing.component.ts new file mode 100644 index 0000000..11a2d0b --- /dev/null +++ b/src/app/pages/level-user-listing/level-user-listing.component.ts @@ -0,0 +1,167 @@ +import {Component, Inject, PLATFORM_ID} from '@angular/core'; +import {ClientService, defaultPageSize} from "../../api/client.service"; +import {ActivatedRoute} from "@angular/router"; +import {PageTitleComponent} from "../../components/ui/text/page-title.component"; +import {ResponsiveGridComponent} from "../../components/ui/responsive-grid.component"; +import {Level} from "../../api/types/levels/level"; +import {LevelPreviewComponent} from "../../components/items/level-preview.component"; +import {ContainerComponent} from "../../components/ui/container.component"; +import {Scrollable} from "../../helpers/scrollable"; +import {defaultListInfo, RefreshApiListInfo} from "../../api/refresh-api-list-info"; +import {InfiniteScrollerComponent} from "../../components/ui/infinite-scroller.component"; +import { User } from '../../api/types/users/user'; +import { UserLinkComponent } from "../../components/ui/text/links/user-link.component"; +import { RadioButtonComponent } from "../../components/ui/form/radio-button.component"; +import { DropdownMenuComponent } from "../../components/ui/form/dropdown-menu.component"; +import { ButtonComponent } from "../../components/ui/form/button.component"; +import { ContainerHeaderComponent } from "../../components/ui/container-header.component"; +import { FormControl, FormGroup } from '@angular/forms'; +import { ListWithData } from '../../api/list-with-data'; +import { BannerService } from '../../banners/banner.service'; +import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; +import { isPlatformBrowser } from '@angular/common'; + +@Component({ + selector: 'app-level-user-listing', + imports: [ + PageTitleComponent, + ResponsiveGridComponent, + LevelPreviewComponent, + ContainerComponent, + InfiniteScrollerComponent, + UserLinkComponent, + RadioButtonComponent, + DropdownMenuComponent, + ButtonComponent, + ContainerHeaderComponent +], + templateUrl: './level-user-listing.component.html' +}) +export class LevelUserListingComponent implements Scrollable { + user: User | undefined; + levelsPublishedByUser: ListWithData | undefined; + levelsHeartedByUser: ListWithData | undefined; + currentLevels: Level[] = []; + currentListInfo: RefreshApiListInfo = defaultListInfo; + + showLevelDropdown: boolean = false; + levelSelectionString: string = ""; + filterForm = new FormGroup({ + selection: new FormControl(-1) + }); + + protected readonly isBrowser: boolean; + + constructor(private client: ClientService, protected banner: BannerService, private route: ActivatedRoute, @Inject(PLATFORM_ID) platformId: Object) { + route.params.subscribe(params => { + const username: string | undefined = params['username']; + const category: string | undefined = params['category']; + + if (username != null) { + this.client.getUserByUsername(username).subscribe(user => { + this.user = user; + + if (category != null) { + this.levelSelectionString = category; + switch (category) { + case "byUser": + this.setLevelSelection(0); + break; + case "hearted": + this.setLevelSelection(1); + break; + default: + this.banner.warn("Cannot get levels", "Selection '" + category + "' is unknown"); + return; + } + } + }); + } + }); + + this.isBrowser = isPlatformBrowser(platformId); + } + + levelSelectionButtonClick() { + this.showLevelDropdown = !this.showLevelDropdown; + } + + getLevelSelectionText(selection: number): string { + switch (selection) { + case 0: return "Published by"; + case 1: return "Hearted by"; + default: "Something by"; + } + return ""; + } + + setLevelSelection(selection: number) { + if (this.user == null) return; + + let previousSelection: number = this.filterForm.controls.selection.getRawValue()!; + if (selection === previousSelection) return; + + switch (previousSelection) { + case 0: + this.levelsPublishedByUser = { + data: this.currentLevels, + listInfo: this.currentListInfo + }; + break; + case 1: + this.levelsHeartedByUser = { + data: this.currentLevels, + listInfo: this.currentListInfo + }; + break; + } + + let cachedList: ListWithData | undefined; + switch (selection) { + case 0: + this.levelSelectionString = "byUser"; + cachedList = this.levelsPublishedByUser; + break; + case 1: + this.levelSelectionString = "hearted"; + cachedList = this.levelsHeartedByUser; + break; + default: + this.banner.warn("Cannot get levels", "Selection " + selection + " is unknown"); + return; + } + + this.filterForm.controls.selection.setValue(selection); + if(this.isBrowser) { + window.history.replaceState({}, '', `/levels/${this.levelSelectionString}/user/${this.user.username}`); + } + + if (cachedList != null) { + this.currentLevels = cachedList.data; + this.listInfo = cachedList.listInfo; + return; + } + + this.currentLevels = []; + this.listInfo = defaultListInfo; + this.loadData(); + } + + isLoading: boolean = false; + listInfo: RefreshApiListInfo = defaultListInfo; + + loadData(): void { + if(!this.user) return; + + this.isLoading = true; + this.client.getLevelsInCategory(this.levelSelectionString, this.listInfo.nextPageIndex, defaultPageSize, {u: this.user.username}).subscribe(list => { + this.isLoading = false; + + this.currentLevels = this.currentLevels.concat(list.data); + this.listInfo = list.listInfo; + }); + } + + protected readonly faChevronDown = faChevronDown; + protected readonly faChevronUp = faChevronUp; +} diff --git a/src/app/pages/photo-user-listing/photo-user-listing.component.html b/src/app/pages/photo-user-listing/photo-user-listing.component.html new file mode 100644 index 0000000..65c861d --- /dev/null +++ b/src/app/pages/photo-user-listing/photo-user-listing.component.html @@ -0,0 +1,35 @@ +@if (user) { + +
+
+ + + + +
+ + +
+
+ +
+
+
+ + @if (currentPhotos) { + + @for (photo of this.currentPhotos; track photo.photoId) { + + + + } + + } + + +} diff --git a/src/app/pages/photo-user-listing/photo-user-listing.component.ts b/src/app/pages/photo-user-listing/photo-user-listing.component.ts new file mode 100644 index 0000000..45430d9 --- /dev/null +++ b/src/app/pages/photo-user-listing/photo-user-listing.component.ts @@ -0,0 +1,167 @@ +import {Component, Inject, PLATFORM_ID} from '@angular/core'; +import {ClientService, defaultPageSize} from "../../api/client.service"; +import {ActivatedRoute} from "@angular/router"; +import {PageTitleComponent} from "../../components/ui/text/page-title.component"; +import {ResponsiveGridComponent} from "../../components/ui/responsive-grid.component"; +import {ContainerComponent} from "../../components/ui/container.component"; +import {Scrollable} from "../../helpers/scrollable"; +import {defaultListInfo, RefreshApiListInfo} from "../../api/refresh-api-list-info"; +import {InfiniteScrollerComponent} from "../../components/ui/infinite-scroller.component"; +import { User } from '../../api/types/users/user'; +import { UserLinkComponent } from "../../components/ui/text/links/user-link.component"; +import { RadioButtonComponent } from "../../components/ui/form/radio-button.component"; +import { DropdownMenuComponent } from "../../components/ui/form/dropdown-menu.component"; +import { ButtonComponent } from "../../components/ui/form/button.component"; +import { ContainerHeaderComponent } from "../../components/ui/container-header.component"; +import { FormControl, FormGroup } from '@angular/forms'; +import { ListWithData } from '../../api/list-with-data'; +import { BannerService } from '../../banners/banner.service'; +import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; +import { Photo } from '../../api/types/photos/photo'; +import { PhotoComponent } from "../../components/items/photo.component"; +import { isPlatformBrowser } from '@angular/common'; + +@Component({ + selector: 'app-photo-user-listing', + imports: [ + PageTitleComponent, + ResponsiveGridComponent, + ContainerComponent, + InfiniteScrollerComponent, + UserLinkComponent, + RadioButtonComponent, + DropdownMenuComponent, + ButtonComponent, + ContainerHeaderComponent, + PhotoComponent +], + templateUrl: './photo-user-listing.component.html' +}) +export class PhotoUserListingComponent implements Scrollable { + user: User | undefined; + photosByUser: ListWithData | undefined; + photosWithUser: ListWithData | undefined; + currentPhotos: Photo[] = []; + currentListInfo: RefreshApiListInfo = defaultListInfo; + + showPhotoDropdown: boolean = false; + photoSelectionString: string = ""; + filterForm = new FormGroup({ + selection: new FormControl(-1) + }); + + protected readonly isBrowser: boolean; + + constructor(private client: ClientService, protected banner: BannerService, private route: ActivatedRoute, @Inject(PLATFORM_ID) platformId: Object) { + route.params.subscribe(params => { + const username: string | undefined = params['username']; + const category: string | undefined = params['category']; + + if (username != null) { + this.client.getUserByUsername(username).subscribe(user => { + this.user = user; + + if (category != null) { + this.photoSelectionString = category; + switch (category) { + case "by": + this.setPhotoSelection(0); + break; + case "with": + this.setPhotoSelection(1); + break; + default: + this.banner.warn("Cannot get photos", "Selection '" + category + "' is unknown"); + return; + } + } + }); + } + }); + + this.isBrowser = isPlatformBrowser(platformId); + } + + photoSelectionButtonClick() { + this.showPhotoDropdown = !this.showPhotoDropdown; + } + + getPhotoSelectionText(selection: number): string { + switch (selection) { + case 0: return "By"; + case 1: return "With"; + default: "Something"; + } + return ""; + } + + setPhotoSelection(selection: number) { + if (this.user == null) return; + + let previousSelection: number = this.filterForm.controls.selection.getRawValue()!; + if (selection === previousSelection) return; + + switch (previousSelection) { + case 0: + this.photosByUser = { + data: this.currentPhotos, + listInfo: this.currentListInfo + }; + break; + case 1: + this.photosWithUser = { + data: this.currentPhotos, + listInfo: this.currentListInfo + }; + break; + } + + let cachedList: ListWithData | undefined; + switch (selection) { + case 0: + this.photoSelectionString = "by"; + cachedList = this.photosByUser; + break; + case 1: + this.photoSelectionString = "with"; + cachedList = this.photosWithUser; + break; + default: + this.banner.warn("Cannot get photos", "Selection " + selection + " is unknown"); + return; + } + + this.filterForm.controls.selection.setValue(selection); + if(this.isBrowser) { + window.history.replaceState({}, '', `/photos/${this.photoSelectionString}/user/${this.user.username}`); + } + + if (cachedList != null) { + this.currentPhotos = cachedList.data; + this.listInfo = cachedList.listInfo; + return; + } + + this.currentPhotos = []; + this.listInfo = defaultListInfo; + this.loadData(); + } + + isLoading: boolean = false; + listInfo: RefreshApiListInfo = defaultListInfo; + + loadData(): void { + if(!this.user) return; + + this.isLoading = true; + this.client.getPhotosRelatedToUserUuid(this.user.userId, this.photoSelectionString, this.listInfo.nextPageIndex, defaultPageSize).subscribe(list => { + this.isLoading = false; + + this.currentPhotos = this.currentPhotos.concat(list.data); + this.listInfo = list.listInfo; + }); + } + + protected readonly faChevronDown = faChevronDown; + protected readonly faChevronUp = faChevronUp; +} diff --git a/src/app/pages/user/user.component.html b/src/app/pages/user/user.component.html index 4028933..32e737a 100644 --- a/src/app/pages/user/user.component.html +++ b/src/app/pages/user/user.component.html @@ -23,22 +23,22 @@
- + Levels - - - -
- - -
+ + + +
+ + +
@@ -58,22 +58,22 @@
- + Photos - - - -
- - -
+ + + +
+ + +
diff --git a/src/app/pages/user/user.component.ts b/src/app/pages/user/user.component.ts index 933be0a..87f68a3 100644 --- a/src/app/pages/user/user.component.ts +++ b/src/app/pages/user/user.component.ts @@ -126,7 +126,7 @@ export class UserComponent { cachedList = this.levelsHeartedByUser; break; default: - this.banner.warn("Cannot get levels", "Selection" + selection + " is unknown"); + this.banner.warn("Cannot get levels", "Selection " + selection + " is unknown"); return; } this.levelForm.controls.selection.setValue(selection); @@ -184,7 +184,7 @@ export class UserComponent { cachedList = this.photosWithUser; break; default: - this.banner.warn("Cannot get photos", "Selection" + selection + " is unknown"); + this.banner.warn("Cannot get photos", "Selection " + selection + " is unknown"); return; } this.photoForm.controls.selection.setValue(selection); From b70395ea19a7e820496f5e21054436898a890a9c Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Wed, 8 Apr 2026 19:35:31 +0200 Subject: [PATCH 03/10] Properly handle errors when getting user or category for level/photo pages --- .../level-user-listing.component.ts | 40 +++++++++++-------- .../photo-user-listing.component.ts | 40 +++++++++++-------- src/app/pages/user/user.component.ts | 6 +-- 3 files changed, 48 insertions(+), 38 deletions(-) diff --git a/src/app/pages/level-user-listing/level-user-listing.component.ts b/src/app/pages/level-user-listing/level-user-listing.component.ts index 11a2d0b..a4e31d9 100644 --- a/src/app/pages/level-user-listing/level-user-listing.component.ts +++ b/src/app/pages/level-user-listing/level-user-listing.component.ts @@ -20,6 +20,7 @@ import { ListWithData } from '../../api/list-with-data'; import { BannerService } from '../../banners/banner.service'; import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; import { isPlatformBrowser } from '@angular/common'; +import { RefreshApiError } from '../../api/refresh-api-error'; @Component({ selector: 'app-level-user-listing', @@ -58,21 +59,27 @@ export class LevelUserListingComponent implements Scrollable { const category: string | undefined = params['category']; if (username != null) { - this.client.getUserByUsername(username).subscribe(user => { - this.user = user; - - if (category != null) { - this.levelSelectionString = category; - switch (category) { - case "byUser": - this.setLevelSelection(0); - break; - case "hearted": - this.setLevelSelection(1); - break; - default: - this.banner.warn("Cannot get levels", "Selection '" + category + "' is unknown"); - return; + this.client.getUserByUsername(username).subscribe({ + error: error => { + const apiError: RefreshApiError | undefined = error.error?.error; + this.banner.warn("Failed to get user", apiError == null ? error.message : apiError.message); + }, + next: user => { + this.user = user; + + if (category != null) { + this.levelSelectionString = category; + switch (category) { + case "byUser": + this.setLevelSelection(0); + break; + case "hearted": + this.setLevelSelection(1); + break; + default: + this.banner.warn("Cannot get levels", "Selection '" + category + "' is unknown"); + return; + } } } }); @@ -90,9 +97,8 @@ export class LevelUserListingComponent implements Scrollable { switch (selection) { case 0: return "Published by"; case 1: return "Hearted by"; - default: "Something by"; + default: return "Something by"; } - return ""; } setLevelSelection(selection: number) { diff --git a/src/app/pages/photo-user-listing/photo-user-listing.component.ts b/src/app/pages/photo-user-listing/photo-user-listing.component.ts index 45430d9..f55e47d 100644 --- a/src/app/pages/photo-user-listing/photo-user-listing.component.ts +++ b/src/app/pages/photo-user-listing/photo-user-listing.component.ts @@ -20,6 +20,7 @@ import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; import { Photo } from '../../api/types/photos/photo'; import { PhotoComponent } from "../../components/items/photo.component"; import { isPlatformBrowser } from '@angular/common'; +import { RefreshApiError } from '../../api/refresh-api-error'; @Component({ selector: 'app-photo-user-listing', @@ -58,21 +59,27 @@ export class PhotoUserListingComponent implements Scrollable { const category: string | undefined = params['category']; if (username != null) { - this.client.getUserByUsername(username).subscribe(user => { - this.user = user; - - if (category != null) { - this.photoSelectionString = category; - switch (category) { - case "by": - this.setPhotoSelection(0); - break; - case "with": - this.setPhotoSelection(1); - break; - default: - this.banner.warn("Cannot get photos", "Selection '" + category + "' is unknown"); - return; + this.client.getUserByUsername(username).subscribe({ + error: error => { + const apiError: RefreshApiError | undefined = error.error?.error; + this.banner.warn("Failed to get user", apiError == null ? error.message : apiError.message); + }, + next: user => { + this.user = user; + + if (category != null) { + this.photoSelectionString = category; + switch (category) { + case "by": + this.setPhotoSelection(0); + break; + case "with": + this.setPhotoSelection(1); + break; + default: + this.banner.warn("Cannot get photos", "Selection '" + category + "' is unknown"); + return; + } } } }); @@ -90,9 +97,8 @@ export class PhotoUserListingComponent implements Scrollable { switch (selection) { case 0: return "By"; case 1: return "With"; - default: "Something"; + default: return "Something"; } - return ""; } setPhotoSelection(selection: number) { diff --git a/src/app/pages/user/user.component.ts b/src/app/pages/user/user.component.ts index 87f68a3..458f7aa 100644 --- a/src/app/pages/user/user.component.ts +++ b/src/app/pages/user/user.component.ts @@ -161,9 +161,8 @@ export class UserComponent { switch (selection) { case 0: return "Published by user"; case 1: return "Hearted by user"; - default: "Something by user"; + default: return "Something by user"; } - return ""; } photoSelectionButtonClick() { @@ -220,9 +219,8 @@ export class UserComponent { switch (selection) { case 0: return "By user"; case 1: return "With user"; - default: "Something user"; + default: return "Something user"; } - return ""; } protected readonly faChevronDown = faChevronDown; From 66c135c8f5d8bf70834305c127f7b277b2947495 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Wed, 8 Apr 2026 19:38:37 +0200 Subject: [PATCH 04/10] minor dropdown menu position adjustment --- .../pages/photo-user-listing/photo-user-listing.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/pages/photo-user-listing/photo-user-listing.component.html b/src/app/pages/photo-user-listing/photo-user-listing.component.html index 65c861d..f66beb2 100644 --- a/src/app/pages/photo-user-listing/photo-user-listing.component.html +++ b/src/app/pages/photo-user-listing/photo-user-listing.component.html @@ -3,7 +3,7 @@
- + Date: Wed, 8 Apr 2026 20:07:06 +0200 Subject: [PATCH 05/10] Use ListWithData more, show level/photo by user total counts --- .../level-user-listing.component.html | 3 +- .../level-user-listing.component.ts | 30 ++++++++-------- .../photo-user-listing.component.html | 3 +- .../photo-user-listing.component.ts | 32 ++++++++--------- src/app/pages/user/user.component.html | 10 +++--- src/app/pages/user/user.component.ts | 36 +++++++++++-------- 6 files changed, 61 insertions(+), 53 deletions(-) diff --git a/src/app/pages/level-user-listing/level-user-listing.component.html b/src/app/pages/level-user-listing/level-user-listing.component.html index 6bb022f..150c363 100644 --- a/src/app/pages/level-user-listing/level-user-listing.component.html +++ b/src/app/pages/level-user-listing/level-user-listing.component.html @@ -17,13 +17,14 @@
+ ({{this.listInfo.totalItems}} in total)
@if (currentLevels) { - @for (level of this.currentLevels; track level.levelId) { + @for (level of this.currentLevels.data; track level.levelId) { diff --git a/src/app/pages/level-user-listing/level-user-listing.component.ts b/src/app/pages/level-user-listing/level-user-listing.component.ts index a4e31d9..1595123 100644 --- a/src/app/pages/level-user-listing/level-user-listing.component.ts +++ b/src/app/pages/level-user-listing/level-user-listing.component.ts @@ -42,7 +42,10 @@ export class LevelUserListingComponent implements Scrollable { user: User | undefined; levelsPublishedByUser: ListWithData | undefined; levelsHeartedByUser: ListWithData | undefined; - currentLevels: Level[] = []; + currentLevels: ListWithData = { + data: [], + listInfo: defaultListInfo, + }; currentListInfo: RefreshApiListInfo = defaultListInfo; showLevelDropdown: boolean = false; @@ -109,16 +112,10 @@ export class LevelUserListingComponent implements Scrollable { switch (previousSelection) { case 0: - this.levelsPublishedByUser = { - data: this.currentLevels, - listInfo: this.currentListInfo - }; + this.levelsPublishedByUser = this.currentLevels; break; case 1: - this.levelsHeartedByUser = { - data: this.currentLevels, - listInfo: this.currentListInfo - }; + this.levelsHeartedByUser = this.currentLevels; break; } @@ -143,18 +140,19 @@ export class LevelUserListingComponent implements Scrollable { } if (cachedList != null) { - this.currentLevels = cachedList.data; - this.listInfo = cachedList.listInfo; + this.currentLevels = cachedList; return; } - this.currentLevels = []; - this.listInfo = defaultListInfo; + this.currentLevels = { + data: [], + listInfo: defaultListInfo, + }; this.loadData(); } isLoading: boolean = false; - listInfo: RefreshApiListInfo = defaultListInfo; + get listInfo() {return this.currentLevels.listInfo} loadData(): void { if(!this.user) return; @@ -163,8 +161,8 @@ export class LevelUserListingComponent implements Scrollable { this.client.getLevelsInCategory(this.levelSelectionString, this.listInfo.nextPageIndex, defaultPageSize, {u: this.user.username}).subscribe(list => { this.isLoading = false; - this.currentLevels = this.currentLevels.concat(list.data); - this.listInfo = list.listInfo; + this.currentLevels.data = this.currentLevels.data.concat(list.data); + this.currentLevels.listInfo = list.listInfo; }); } diff --git a/src/app/pages/photo-user-listing/photo-user-listing.component.html b/src/app/pages/photo-user-listing/photo-user-listing.component.html index f66beb2..a0787bf 100644 --- a/src/app/pages/photo-user-listing/photo-user-listing.component.html +++ b/src/app/pages/photo-user-listing/photo-user-listing.component.html @@ -17,13 +17,14 @@ + ({{this.currentPhotos.listInfo.totalItems}} in total) @if (currentPhotos) { - @for (photo of this.currentPhotos; track photo.photoId) { + @for (photo of this.currentPhotos.data; track photo.photoId) { diff --git a/src/app/pages/photo-user-listing/photo-user-listing.component.ts b/src/app/pages/photo-user-listing/photo-user-listing.component.ts index f55e47d..543a907 100644 --- a/src/app/pages/photo-user-listing/photo-user-listing.component.ts +++ b/src/app/pages/photo-user-listing/photo-user-listing.component.ts @@ -42,7 +42,10 @@ export class PhotoUserListingComponent implements Scrollable { user: User | undefined; photosByUser: ListWithData | undefined; photosWithUser: ListWithData | undefined; - currentPhotos: Photo[] = []; + currentPhotos: ListWithData = { + data: [], + listInfo: defaultListInfo, + }; currentListInfo: RefreshApiListInfo = defaultListInfo; showPhotoDropdown: boolean = false; @@ -109,16 +112,10 @@ export class PhotoUserListingComponent implements Scrollable { switch (previousSelection) { case 0: - this.photosByUser = { - data: this.currentPhotos, - listInfo: this.currentListInfo - }; + this.photosByUser = this.currentPhotos; break; case 1: - this.photosWithUser = { - data: this.currentPhotos, - listInfo: this.currentListInfo - }; + this.photosWithUser = this.currentPhotos; break; } @@ -143,28 +140,29 @@ export class PhotoUserListingComponent implements Scrollable { } if (cachedList != null) { - this.currentPhotos = cachedList.data; - this.listInfo = cachedList.listInfo; + this.currentPhotos = cachedList; return; } - this.currentPhotos = []; - this.listInfo = defaultListInfo; + this.currentPhotos = { + data: [], + listInfo: defaultListInfo, + }; this.loadData(); } isLoading: boolean = false; - listInfo: RefreshApiListInfo = defaultListInfo; + get listInfo() {return this.currentPhotos.listInfo} loadData(): void { if(!this.user) return; this.isLoading = true; - this.client.getPhotosRelatedToUserUuid(this.user.userId, this.photoSelectionString, this.listInfo.nextPageIndex, defaultPageSize).subscribe(list => { + this.client.getPhotosRelatedToUserUuid(this.user.userId, this.photoSelectionString, this.currentPhotos.listInfo.nextPageIndex, defaultPageSize).subscribe(list => { this.isLoading = false; - this.currentPhotos = this.currentPhotos.concat(list.data); - this.listInfo = list.listInfo; + this.currentPhotos.data = this.currentPhotos.data.concat(list.data); + this.currentPhotos.listInfo = list.listInfo; }); } diff --git a/src/app/pages/user/user.component.html b/src/app/pages/user/user.component.html index 32e737a..4a4f4db 100644 --- a/src/app/pages/user/user.component.html +++ b/src/app/pages/user/user.component.html @@ -26,6 +26,7 @@ Levels + ({{this.currentLevels.listInfo.totalItems}} in total) - @if (currentLevels.length > 0) { - @for (level of currentLevels; track level.levelId; let i = $index) { + @if (currentLevels.data.length > 0) { + @for (level of currentLevels.data; track level.levelId; let i = $index) {
@@ -61,6 +62,7 @@ Photos + ({{this.currentPhotos.listInfo.totalItems}} in total)
- @if (currentPhotos.length > 0) { - @for (photo of currentPhotos; track photo.photoId; let i = $index) { + @if (currentPhotos.data.length > 0) { + @for (photo of currentPhotos.data; track photo.photoId; let i = $index) {
diff --git a/src/app/pages/user/user.component.ts b/src/app/pages/user/user.component.ts index 458f7aa..034b8e9 100644 --- a/src/app/pages/user/user.component.ts +++ b/src/app/pages/user/user.component.ts @@ -30,6 +30,8 @@ import { RefreshApiError } from '../../api/refresh-api-error'; import { LevelPreviewComponent } from "../../components/items/level-preview.component"; import { PhotoComponent } from "../../components/items/photo.component"; import { DarkContainerComponent } from "../../components/ui/dark-container.component"; +import { ListWithData } from '../../api/list-with-data'; +import { defaultListInfo } from '../../api/refresh-api-list-info'; @Component({ selector: 'app-user', @@ -63,9 +65,12 @@ export class UserComponent { protected ownUser: ExtendedUser | undefined; protected isMobile: boolean = false; - levelsPublishedByUser: Level[] | undefined; - levelsHeartedByUser: Level[] | undefined; - currentLevels: Level[] = []; + levelsPublishedByUser: ListWithData | undefined; + levelsHeartedByUser: ListWithData | undefined; + currentLevels: ListWithData = { + data: [], + listInfo: defaultListInfo, + }; showLevelDropdown: boolean = false; levelSelectionString: string = ""; @@ -73,9 +78,12 @@ export class UserComponent { selection: new FormControl(0) }); - photosByUser: Photo[] | undefined; - photosWithUser: Photo[] | undefined; - currentPhotos: Photo[] = []; + photosByUser: ListWithData | undefined; + photosWithUser: ListWithData | undefined; + currentPhotos: ListWithData = { + data: [], + listInfo: defaultListInfo, + }; showPhotoDropdown: boolean = false; photoSelectionString: string = ""; @@ -114,7 +122,7 @@ export class UserComponent { getLevels(selection: number) { if (this.user == null) return; - let cachedList: Level[] | undefined; + let cachedList: ListWithData | undefined; switch (selection) { case 0: @@ -145,14 +153,14 @@ export class UserComponent { // cache the page switch (selection) { case 0: - this.levelsPublishedByUser = levelPage.data; + this.levelsPublishedByUser = levelPage; break; case 1: - this.levelsHeartedByUser = levelPage.data; + this.levelsHeartedByUser = levelPage; break; } - this.currentLevels = levelPage.data; + this.currentLevels = levelPage; } }); } @@ -171,7 +179,7 @@ export class UserComponent { getPhotos(selection: number) { if (this.user == null) return; - let cachedList: Photo[] | undefined; + let cachedList: ListWithData | undefined; switch (selection) { case 0: @@ -203,14 +211,14 @@ export class UserComponent { // cache the page switch (selection) { case 0: - this.photosByUser = photoPage.data; + this.photosByUser = photoPage; break; case 1: - this.photosWithUser = photoPage.data; + this.photosWithUser = photoPage; break; } - this.currentPhotos = photoPage.data; + this.currentPhotos = photoPage; } }); } From 295debcf5f2f1074252e1b10d6c76b45f8529ab1 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Wed, 8 Apr 2026 21:09:28 +0200 Subject: [PATCH 06/10] Show whether level/photo by user lists are still loading, show more error messages --- .../level-user-listing.component.html | 8 +++++++- .../level-user-listing.component.ts | 16 +++++++++++----- .../photo-user-listing.component.html | 8 +++++++- .../photo-user-listing.component.ts | 16 +++++++++++----- src/app/pages/user/user.component.html | 7 ++++++- 5 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/app/pages/level-user-listing/level-user-listing.component.html b/src/app/pages/level-user-listing/level-user-listing.component.html index 150c363..b9883e8 100644 --- a/src/app/pages/level-user-listing/level-user-listing.component.html +++ b/src/app/pages/level-user-listing/level-user-listing.component.html @@ -22,7 +22,7 @@ - @if (currentLevels) { + @if (currentLevels.data.length > 0) { @for (level of this.currentLevels.data; track level.levelId) { @@ -31,6 +31,12 @@ } } + @else if (currentLevels.listInfo.totalItems < 0) { +

Loading levels...

+ } + @else { +

No levels yet...

+ } } diff --git a/src/app/pages/level-user-listing/level-user-listing.component.ts b/src/app/pages/level-user-listing/level-user-listing.component.ts index 1595123..aa17a40 100644 --- a/src/app/pages/level-user-listing/level-user-listing.component.ts +++ b/src/app/pages/level-user-listing/level-user-listing.component.ts @@ -158,11 +158,17 @@ export class LevelUserListingComponent implements Scrollable { if(!this.user) return; this.isLoading = true; - this.client.getLevelsInCategory(this.levelSelectionString, this.listInfo.nextPageIndex, defaultPageSize, {u: this.user.username}).subscribe(list => { - this.isLoading = false; - - this.currentLevels.data = this.currentLevels.data.concat(list.data); - this.currentLevels.listInfo = list.listInfo; + this.client.getLevelsInCategory(this.levelSelectionString, this.listInfo.nextPageIndex, defaultPageSize, {u: this.user.username}).subscribe({ + error: error => { + const apiError: RefreshApiError | undefined = error.error?.error; + this.banner.warn("Failed to get levels", apiError == null ? error.message : apiError.message); + }, + next: list => { + this.isLoading = false; + + this.currentLevels.data = this.currentLevels.data.concat(list.data); + this.currentLevels.listInfo = list.listInfo; + } }); } diff --git a/src/app/pages/photo-user-listing/photo-user-listing.component.html b/src/app/pages/photo-user-listing/photo-user-listing.component.html index a0787bf..6e2939f 100644 --- a/src/app/pages/photo-user-listing/photo-user-listing.component.html +++ b/src/app/pages/photo-user-listing/photo-user-listing.component.html @@ -22,7 +22,7 @@ - @if (currentPhotos) { + @if (currentPhotos.data.length > 0) { @for (photo of this.currentPhotos.data; track photo.photoId) { @@ -31,6 +31,12 @@ } } + @else if (currentPhotos.listInfo.totalItems < 0) { +

Loading photos...

+ } + @else { +

No photos yet...

+ } } diff --git a/src/app/pages/photo-user-listing/photo-user-listing.component.ts b/src/app/pages/photo-user-listing/photo-user-listing.component.ts index 543a907..518f68b 100644 --- a/src/app/pages/photo-user-listing/photo-user-listing.component.ts +++ b/src/app/pages/photo-user-listing/photo-user-listing.component.ts @@ -158,11 +158,17 @@ export class PhotoUserListingComponent implements Scrollable { if(!this.user) return; this.isLoading = true; - this.client.getPhotosRelatedToUserUuid(this.user.userId, this.photoSelectionString, this.currentPhotos.listInfo.nextPageIndex, defaultPageSize).subscribe(list => { - this.isLoading = false; - - this.currentPhotos.data = this.currentPhotos.data.concat(list.data); - this.currentPhotos.listInfo = list.listInfo; + this.client.getPhotosRelatedToUserUuid(this.user.userId, this.photoSelectionString, this.currentPhotos.listInfo.nextPageIndex, defaultPageSize).subscribe({ + error: error => { + const apiError: RefreshApiError | undefined = error.error?.error; + this.banner.warn("Failed to get photos", apiError == null ? error.message : apiError.message); + }, + next: list => { + this.isLoading = false; + + this.currentPhotos.data = this.currentPhotos.data.concat(list.data); + this.currentPhotos.listInfo = list.listInfo; + } }); } diff --git a/src/app/pages/user/user.component.html b/src/app/pages/user/user.component.html index 4a4f4db..9ab9577 100644 --- a/src/app/pages/user/user.component.html +++ b/src/app/pages/user/user.component.html @@ -52,6 +52,9 @@ } } + @else if (currentLevels.listInfo.totalItems < 0) { +

Loading levels...

+ } @else {

No levels yet...

} @@ -85,7 +88,9 @@ } - + } + @else if (currentPhotos.listInfo.totalItems < 0) { +

Loading photos...

} @else {

No photos yet...

From 4f9ff72e5acee199e2733d2368082b94a7f639bc Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Fri, 10 Apr 2026 11:32:37 +0200 Subject: [PATCH 07/10] Remove no longer used attributes --- src/app/pages/level-user-listing/level-user-listing.component.ts | 1 - src/app/pages/photo-user-listing/photo-user-listing.component.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/app/pages/level-user-listing/level-user-listing.component.ts b/src/app/pages/level-user-listing/level-user-listing.component.ts index aa17a40..931a99c 100644 --- a/src/app/pages/level-user-listing/level-user-listing.component.ts +++ b/src/app/pages/level-user-listing/level-user-listing.component.ts @@ -46,7 +46,6 @@ export class LevelUserListingComponent implements Scrollable { data: [], listInfo: defaultListInfo, }; - currentListInfo: RefreshApiListInfo = defaultListInfo; showLevelDropdown: boolean = false; levelSelectionString: string = ""; diff --git a/src/app/pages/photo-user-listing/photo-user-listing.component.ts b/src/app/pages/photo-user-listing/photo-user-listing.component.ts index 518f68b..b548a4d 100644 --- a/src/app/pages/photo-user-listing/photo-user-listing.component.ts +++ b/src/app/pages/photo-user-listing/photo-user-listing.component.ts @@ -46,7 +46,6 @@ export class PhotoUserListingComponent implements Scrollable { data: [], listInfo: defaultListInfo, }; - currentListInfo: RefreshApiListInfo = defaultListInfo; showPhotoDropdown: boolean = false; photoSelectionString: string = ""; From 0c839eab1d4fa92882bfe8735b1c2ce08f63de1d Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Sun, 12 Apr 2026 20:59:23 +0200 Subject: [PATCH 08/10] github merge fail fix --- src/app/pages/user/user.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/pages/user/user.component.ts b/src/app/pages/user/user.component.ts index 2e7545b..db02410 100644 --- a/src/app/pages/user/user.component.ts +++ b/src/app/pages/user/user.component.ts @@ -55,8 +55,8 @@ import { UserRoleComponent } from "../../components/ui/info/user-role.component" DividerComponent, LevelPreviewComponent, PhotoComponent, - DarkContainerComponent - UserRoleComponent + DarkContainerComponent, + UserRoleComponent, ], templateUrl: './user.component.html', styles: `` From 63de953722cc54c1c8bd2aa5273ce92426959d40 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 13 Apr 2026 17:47:12 +0200 Subject: [PATCH 09/10] Fix level/photo user listings not loading pages in certain cases --- src/app/api/cached-list-with-data.ts | 5 +++++ .../ui/infinite-scroller.component.ts | 4 +++- .../level-user-listing.component.html | 2 +- .../level-user-listing.component.ts | 19 +++++++++++++------ .../photo-user-listing.component.html | 2 +- .../photo-user-listing.component.ts | 16 ++++++++++++---- 6 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 src/app/api/cached-list-with-data.ts diff --git a/src/app/api/cached-list-with-data.ts b/src/app/api/cached-list-with-data.ts new file mode 100644 index 0000000..31be4d3 --- /dev/null +++ b/src/app/api/cached-list-with-data.ts @@ -0,0 +1,5 @@ +import { ListWithData } from "./list-with-data"; + +export interface CachedListWithData extends ListWithData { + totalLoads: number; +} \ No newline at end of file diff --git a/src/app/components/ui/infinite-scroller.component.ts b/src/app/components/ui/infinite-scroller.component.ts index 59056af..d8e633b 100644 --- a/src/app/components/ui/infinite-scroller.component.ts +++ b/src/app/components/ui/infinite-scroller.component.ts @@ -42,7 +42,8 @@ export class InfiniteScrollerComponent implements AfterViewInit { nextPageIndex: number = this.pageSize + 1; total: number = 0; - totalLoads: number = 0; + @Input() totalLoads: number = 0; + @Output() incrementLoads = new EventEmitter; @Input({required: true}) set listInfo(listInfo: RefreshApiListInfo) { if(!listInfo) return; @@ -56,6 +57,7 @@ export class InfiniteScrollerComponent implements AfterViewInit { this.loadData.emit(); // tell the parent to load more data this.totalLoads++; + this.incrementLoads.emit(); } ngAfterViewInit(): void { diff --git a/src/app/pages/level-user-listing/level-user-listing.component.html b/src/app/pages/level-user-listing/level-user-listing.component.html index b9883e8..d7b8c37 100644 --- a/src/app/pages/level-user-listing/level-user-listing.component.html +++ b/src/app/pages/level-user-listing/level-user-listing.component.html @@ -38,5 +38,5 @@

No levels yet...

} - + } diff --git a/src/app/pages/level-user-listing/level-user-listing.component.ts b/src/app/pages/level-user-listing/level-user-listing.component.ts index 931a99c..4e7dcc0 100644 --- a/src/app/pages/level-user-listing/level-user-listing.component.ts +++ b/src/app/pages/level-user-listing/level-user-listing.component.ts @@ -21,6 +21,7 @@ import { BannerService } from '../../banners/banner.service'; import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; import { isPlatformBrowser } from '@angular/common'; import { RefreshApiError } from '../../api/refresh-api-error'; +import { CachedListWithData } from '../../api/cached-list-with-data'; @Component({ selector: 'app-level-user-listing', @@ -40,11 +41,12 @@ import { RefreshApiError } from '../../api/refresh-api-error'; }) export class LevelUserListingComponent implements Scrollable { user: User | undefined; - levelsPublishedByUser: ListWithData | undefined; - levelsHeartedByUser: ListWithData | undefined; - currentLevels: ListWithData = { + levelsPublishedByUser: CachedListWithData | undefined; + levelsHeartedByUser: CachedListWithData | undefined; + currentLevels: CachedListWithData = { data: [], listInfo: defaultListInfo, + totalLoads: 0, }; showLevelDropdown: boolean = false; @@ -118,7 +120,7 @@ export class LevelUserListingComponent implements Scrollable { break; } - let cachedList: ListWithData | undefined; + let cachedList: CachedListWithData | undefined; switch (selection) { case 0: this.levelSelectionString = "byUser"; @@ -146,7 +148,9 @@ export class LevelUserListingComponent implements Scrollable { this.currentLevels = { data: [], listInfo: defaultListInfo, + totalLoads: 0, }; + this.currentLevels.totalLoads++; this.loadData(); } @@ -163,14 +167,17 @@ export class LevelUserListingComponent implements Scrollable { this.banner.warn("Failed to get levels", apiError == null ? error.message : apiError.message); }, next: list => { - this.isLoading = false; - this.currentLevels.data = this.currentLevels.data.concat(list.data); this.currentLevels.listInfo = list.listInfo; + this.isLoading = false; } }); } + incrementLoads() { + this.currentLevels.totalLoads++; + } + protected readonly faChevronDown = faChevronDown; protected readonly faChevronUp = faChevronUp; } diff --git a/src/app/pages/photo-user-listing/photo-user-listing.component.html b/src/app/pages/photo-user-listing/photo-user-listing.component.html index 6e2939f..02b5a53 100644 --- a/src/app/pages/photo-user-listing/photo-user-listing.component.html +++ b/src/app/pages/photo-user-listing/photo-user-listing.component.html @@ -38,5 +38,5 @@

No photos yet...

} - + } diff --git a/src/app/pages/photo-user-listing/photo-user-listing.component.ts b/src/app/pages/photo-user-listing/photo-user-listing.component.ts index b548a4d..bd8fd93 100644 --- a/src/app/pages/photo-user-listing/photo-user-listing.component.ts +++ b/src/app/pages/photo-user-listing/photo-user-listing.component.ts @@ -21,6 +21,7 @@ import { Photo } from '../../api/types/photos/photo'; import { PhotoComponent } from "../../components/items/photo.component"; import { isPlatformBrowser } from '@angular/common'; import { RefreshApiError } from '../../api/refresh-api-error'; +import { CachedListWithData } from '../../api/cached-list-with-data'; @Component({ selector: 'app-photo-user-listing', @@ -40,11 +41,12 @@ import { RefreshApiError } from '../../api/refresh-api-error'; }) export class PhotoUserListingComponent implements Scrollable { user: User | undefined; - photosByUser: ListWithData | undefined; - photosWithUser: ListWithData | undefined; - currentPhotos: ListWithData = { + photosByUser: CachedListWithData | undefined; + photosWithUser: CachedListWithData | undefined; + currentPhotos: CachedListWithData = { data: [], listInfo: defaultListInfo, + totalLoads: 0, }; showPhotoDropdown: boolean = false; @@ -118,7 +120,7 @@ export class PhotoUserListingComponent implements Scrollable { break; } - let cachedList: ListWithData | undefined; + let cachedList: CachedListWithData | undefined; switch (selection) { case 0: this.photoSelectionString = "by"; @@ -146,7 +148,9 @@ export class PhotoUserListingComponent implements Scrollable { this.currentPhotos = { data: [], listInfo: defaultListInfo, + totalLoads: 0, }; + this.currentPhotos.totalLoads++; this.loadData(); } @@ -171,6 +175,10 @@ export class PhotoUserListingComponent implements Scrollable { }); } + incrementLoads() { + this.currentPhotos.totalLoads++; + } + protected readonly faChevronDown = faChevronDown; protected readonly faChevronUp = faChevronUp; } From f570d5f0ef6de6a7f8a5328e8ba895582d363eae Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Mon, 13 Apr 2026 17:58:11 +0200 Subject: [PATCH 10/10] Update routing titles --- src/app/app.routes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 0b5c126..a6eec10 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -20,7 +20,7 @@ export const routes: Routes = [ { path: 'levels/:category/user/:username', loadComponent: () => import('./pages/level-user-listing/level-user-listing.component').then(x => x.LevelUserListingComponent), - data: {title: "Level Listing"} + data: {title: "Levels Related To User"} }, { path: 'level/:id/:slug', @@ -53,7 +53,7 @@ export const routes: Routes = [ { path: 'photos/:category/user/:username', loadComponent: () => import('./pages/photo-user-listing/photo-user-listing.component').then(x => x.PhotoUserListingComponent), - data: {title: "Photos"}, + data: {title: "Photos Related To User"}, }, { path: 'photo/:id',