Skip to content

Commit e8e3f93

Browse files
authored
Merge pull request #206 from Toastbrot236/user-prof
Show levels and photos on user pages
2 parents 039f4a3 + f570d5f commit e8e3f93

File tree

10 files changed

+731
-8
lines changed

10 files changed

+731
-8
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { ListWithData } from "./list-with-data";
2+
3+
export interface CachedListWithData<TData> extends ListWithData<TData> {
4+
totalLoads: number;
5+
}

src/app/api/client.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ export class ClientService extends ApiImplementation {
148148
return this.http.get<ListWithData<Photo>>(`/photos`, {params: this.createPageQuery(skip, count)});
149149
}
150150

151+
getPhotosRelatedToUserUuid(userId: string, category: string, skip: number = 0, count: number = defaultPageSize) {
152+
return this.http.get<ListWithData<Photo>>(`/photos/${category}/uuid/${userId}`, {params: this.createPageQuery(skip, count)});
153+
}
154+
151155
getContests() {
152156
return this.http.get<ListWithData<Contest>>("/contests");
153157
}

src/app/app.routes.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export const routes: Routes = [
1717
loadComponent: () => import('./pages/level-listing/level-listing.component').then(x => x.LevelListingComponent),
1818
data: {title: "Level Listing"}
1919
},
20+
{
21+
path: 'levels/:category/user/:username',
22+
loadComponent: () => import('./pages/level-user-listing/level-user-listing.component').then(x => x.LevelUserListingComponent),
23+
data: {title: "Levels Related To User"}
24+
},
2025
{
2126
path: 'level/:id/:slug',
2227
loadComponent: () => import('./pages/level/level.component').then(x => x.LevelComponent),
@@ -46,9 +51,9 @@ export const routes: Routes = [
4651
data: {title: "Photos"},
4752
},
4853
{
49-
path: 'photos',
50-
loadComponent: () => import('./pages/photo-listing/photo-listing.component').then(x => x.PhotoListingComponent),
51-
data: {title: "Photos"},
54+
path: 'photos/:category/user/:username',
55+
loadComponent: () => import('./pages/photo-user-listing/photo-user-listing.component').then(x => x.PhotoUserListingComponent),
56+
data: {title: "Photos Related To User"},
5257
},
5358
{
5459
path: 'photo/:id',

src/app/components/ui/infinite-scroller.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ export class InfiniteScrollerComponent implements AfterViewInit {
4242

4343
nextPageIndex: number = this.pageSize + 1;
4444
total: number = 0;
45-
totalLoads: number = 0;
45+
@Input() totalLoads: number = 0;
46+
@Output() incrementLoads = new EventEmitter;
4647

4748
@Input({required: true}) set listInfo(listInfo: RefreshApiListInfo) {
4849
if(!listInfo) return;
@@ -56,6 +57,7 @@ export class InfiniteScrollerComponent implements AfterViewInit {
5657

5758
this.loadData.emit(); // tell the parent to load more data
5859
this.totalLoads++;
60+
this.incrementLoads.emit();
5961
}
6062

6163
ngAfterViewInit(): void {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
@if (user) {
2+
<app-container-header>
3+
<div class="flex flex-col gap-y-2 pb-4">
4+
<div class="flex flex-row flex-wrap gap-x-3 align-center">
5+
<app-page-title title="Levels "></app-page-title>
6+
<app-dropdown-menu [showMenu]="showLevelDropdown" offsets="top-9 -left-5" [width]="56">
7+
<app-button trigger
8+
width="flex flex-shrink justify-evenly flex-grow w-42 text-nowrap"
9+
[text]="getLevelSelectionText(filterForm.controls.selection.getRawValue()!)"
10+
[icon]="showLevelDropdown ? faChevronUp : faChevronDown"
11+
color="bg-secondary"
12+
(click)="levelSelectionButtonClick()">
13+
</app-button>
14+
<div content>
15+
<app-radio-button [label]="getLevelSelectionText(0)" [form]="filterForm" ctrlName="selection" id="l0" [value]='0' (click)="setLevelSelection(0)"></app-radio-button>
16+
<app-radio-button [label]="getLevelSelectionText(1)" [form]="filterForm" ctrlName="selection" id="l1" [value]='1' (click)="setLevelSelection(1)"></app-radio-button>
17+
</div>
18+
</app-dropdown-menu>
19+
<app-user-link class="content-center text-[20px]" [user]="user"></app-user-link>
20+
<span class="content-center text-sm italic text-gentle text-base">({{this.listInfo.totalItems}} in total)</span>
21+
</div>
22+
</div>
23+
</app-container-header>
24+
25+
@if (currentLevels.data.length > 0) {
26+
<app-responsive-grid>
27+
@for (level of this.currentLevels.data; track level.levelId) {
28+
<app-container [tight]="true">
29+
<app-level-preview [level]="level"></app-level-preview>
30+
</app-container>
31+
}
32+
</app-responsive-grid>
33+
}
34+
@else if (currentLevels.listInfo.totalItems < 0) {
35+
<p>Loading levels...</p>
36+
}
37+
@else {
38+
<p>No levels yet...</p>
39+
}
40+
41+
<app-infinite-scroller [isLoading]="this.isLoading" [listInfo]="this.listInfo" [totalLoads]="currentLevels.totalLoads" (incrementLoads)="incrementLoads()" (loadData)="loadData()"></app-infinite-scroller>
42+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import {Component, Inject, PLATFORM_ID} from '@angular/core';
2+
import {ClientService, defaultPageSize} from "../../api/client.service";
3+
import {ActivatedRoute} from "@angular/router";
4+
import {PageTitleComponent} from "../../components/ui/text/page-title.component";
5+
import {ResponsiveGridComponent} from "../../components/ui/responsive-grid.component";
6+
import {Level} from "../../api/types/levels/level";
7+
import {LevelPreviewComponent} from "../../components/items/level-preview.component";
8+
import {ContainerComponent} from "../../components/ui/container.component";
9+
import {Scrollable} from "../../helpers/scrollable";
10+
import {defaultListInfo, RefreshApiListInfo} from "../../api/refresh-api-list-info";
11+
import {InfiniteScrollerComponent} from "../../components/ui/infinite-scroller.component";
12+
import { User } from '../../api/types/users/user';
13+
import { UserLinkComponent } from "../../components/ui/text/links/user-link.component";
14+
import { RadioButtonComponent } from "../../components/ui/form/radio-button.component";
15+
import { DropdownMenuComponent } from "../../components/ui/form/dropdown-menu.component";
16+
import { ButtonComponent } from "../../components/ui/form/button.component";
17+
import { ContainerHeaderComponent } from "../../components/ui/container-header.component";
18+
import { FormControl, FormGroup } from '@angular/forms';
19+
import { ListWithData } from '../../api/list-with-data';
20+
import { BannerService } from '../../banners/banner.service';
21+
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
22+
import { isPlatformBrowser } from '@angular/common';
23+
import { RefreshApiError } from '../../api/refresh-api-error';
24+
import { CachedListWithData } from '../../api/cached-list-with-data';
25+
26+
@Component({
27+
selector: 'app-level-user-listing',
28+
imports: [
29+
PageTitleComponent,
30+
ResponsiveGridComponent,
31+
LevelPreviewComponent,
32+
ContainerComponent,
33+
InfiniteScrollerComponent,
34+
UserLinkComponent,
35+
RadioButtonComponent,
36+
DropdownMenuComponent,
37+
ButtonComponent,
38+
ContainerHeaderComponent
39+
],
40+
templateUrl: './level-user-listing.component.html'
41+
})
42+
export class LevelUserListingComponent implements Scrollable {
43+
user: User | undefined;
44+
levelsPublishedByUser: CachedListWithData<Level> | undefined;
45+
levelsHeartedByUser: CachedListWithData<Level> | undefined;
46+
currentLevels: CachedListWithData<Level> = {
47+
data: [],
48+
listInfo: defaultListInfo,
49+
totalLoads: 0,
50+
};
51+
52+
showLevelDropdown: boolean = false;
53+
levelSelectionString: string = "";
54+
filterForm = new FormGroup({
55+
selection: new FormControl(-1)
56+
});
57+
58+
protected readonly isBrowser: boolean;
59+
60+
constructor(private client: ClientService, protected banner: BannerService, private route: ActivatedRoute, @Inject(PLATFORM_ID) platformId: Object) {
61+
route.params.subscribe(params => {
62+
const username: string | undefined = params['username'];
63+
const category: string | undefined = params['category'];
64+
65+
if (username != null) {
66+
this.client.getUserByUsername(username).subscribe({
67+
error: error => {
68+
const apiError: RefreshApiError | undefined = error.error?.error;
69+
this.banner.warn("Failed to get user", apiError == null ? error.message : apiError.message);
70+
},
71+
next: user => {
72+
this.user = user;
73+
74+
if (category != null) {
75+
this.levelSelectionString = category;
76+
switch (category) {
77+
case "byUser":
78+
this.setLevelSelection(0);
79+
break;
80+
case "hearted":
81+
this.setLevelSelection(1);
82+
break;
83+
default:
84+
this.banner.warn("Cannot get levels", "Selection '" + category + "' is unknown");
85+
return;
86+
}
87+
}
88+
}
89+
});
90+
}
91+
});
92+
93+
this.isBrowser = isPlatformBrowser(platformId);
94+
}
95+
96+
levelSelectionButtonClick() {
97+
this.showLevelDropdown = !this.showLevelDropdown;
98+
}
99+
100+
getLevelSelectionText(selection: number): string {
101+
switch (selection) {
102+
case 0: return "Published by";
103+
case 1: return "Hearted by";
104+
default: return "Something by";
105+
}
106+
}
107+
108+
setLevelSelection(selection: number) {
109+
if (this.user == null) return;
110+
111+
let previousSelection: number = this.filterForm.controls.selection.getRawValue()!;
112+
if (selection === previousSelection) return;
113+
114+
switch (previousSelection) {
115+
case 0:
116+
this.levelsPublishedByUser = this.currentLevels;
117+
break;
118+
case 1:
119+
this.levelsHeartedByUser = this.currentLevels;
120+
break;
121+
}
122+
123+
let cachedList: CachedListWithData<Level> | undefined;
124+
switch (selection) {
125+
case 0:
126+
this.levelSelectionString = "byUser";
127+
cachedList = this.levelsPublishedByUser;
128+
break;
129+
case 1:
130+
this.levelSelectionString = "hearted";
131+
cachedList = this.levelsHeartedByUser;
132+
break;
133+
default:
134+
this.banner.warn("Cannot get levels", "Selection " + selection + " is unknown");
135+
return;
136+
}
137+
138+
this.filterForm.controls.selection.setValue(selection);
139+
if(this.isBrowser) {
140+
window.history.replaceState({}, '', `/levels/${this.levelSelectionString}/user/${this.user.username}`);
141+
}
142+
143+
if (cachedList != null) {
144+
this.currentLevels = cachedList;
145+
return;
146+
}
147+
148+
this.currentLevels = {
149+
data: [],
150+
listInfo: defaultListInfo,
151+
totalLoads: 0,
152+
};
153+
this.currentLevels.totalLoads++;
154+
this.loadData();
155+
}
156+
157+
isLoading: boolean = false;
158+
get listInfo() {return this.currentLevels.listInfo}
159+
160+
loadData(): void {
161+
if(!this.user) return;
162+
163+
this.isLoading = true;
164+
this.client.getLevelsInCategory(this.levelSelectionString, this.listInfo.nextPageIndex, defaultPageSize, {u: this.user.username}).subscribe({
165+
error: error => {
166+
const apiError: RefreshApiError | undefined = error.error?.error;
167+
this.banner.warn("Failed to get levels", apiError == null ? error.message : apiError.message);
168+
},
169+
next: list => {
170+
this.currentLevels.data = this.currentLevels.data.concat(list.data);
171+
this.currentLevels.listInfo = list.listInfo;
172+
this.isLoading = false;
173+
}
174+
});
175+
}
176+
177+
incrementLoads() {
178+
this.currentLevels.totalLoads++;
179+
}
180+
181+
protected readonly faChevronDown = faChevronDown;
182+
protected readonly faChevronUp = faChevronUp;
183+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
@if (user) {
2+
<app-container-header>
3+
<div class="flex flex-col gap-y-2 pb-4">
4+
<div class="flex flex-row flex-wrap gap-x-3 align-center">
5+
<app-page-title title="Photos "></app-page-title>
6+
<app-dropdown-menu [showMenu]="showPhotoDropdown" offsets="top-9" [width]="56">
7+
<app-button trigger
8+
width="flex flex-shrink justify-evenly flex-grow w-42 text-nowrap"
9+
[text]="getPhotoSelectionText(filterForm.controls.selection.getRawValue()!)"
10+
[icon]="showPhotoDropdown ? faChevronUp : faChevronDown"
11+
color="bg-secondary"
12+
(click)="photoSelectionButtonClick()">
13+
</app-button>
14+
<div content>
15+
<app-radio-button [label]="getPhotoSelectionText(0)" [form]="filterForm" ctrlName="selection" id="p0" [value]='0' (click)="setPhotoSelection(0)"></app-radio-button>
16+
<app-radio-button [label]="getPhotoSelectionText(1)" [form]="filterForm" ctrlName="selection" id="p1" [value]='1' (click)="setPhotoSelection(1)"></app-radio-button>
17+
</div>
18+
</app-dropdown-menu>
19+
<app-user-link class="content-center text-[20px]" [user]="user"></app-user-link>
20+
<span class="content-center text-sm italic text-gentle text-base">({{this.currentPhotos.listInfo.totalItems}} in total)</span>
21+
</div>
22+
</div>
23+
</app-container-header>
24+
25+
@if (currentPhotos.data.length > 0) {
26+
<app-responsive-grid>
27+
@for (photo of this.currentPhotos.data; track photo.photoId) {
28+
<app-container [tight]="true">
29+
<app-photo [photo]="photo" [link]="true"></app-photo>
30+
</app-container>
31+
}
32+
</app-responsive-grid>
33+
}
34+
@else if (currentPhotos.listInfo.totalItems < 0) {
35+
<p>Loading photos...</p>
36+
}
37+
@else {
38+
<p>No photos yet...</p>
39+
}
40+
41+
<app-infinite-scroller [isLoading]="this.isLoading" [listInfo]="this.listInfo" [totalLoads]="currentPhotos.totalLoads" (incrementLoads)="incrementLoads()" (loadData)="loadData()"></app-infinite-scroller>
42+
}

0 commit comments

Comments
 (0)