Profile page, tweaked banlist/follows

This commit is contained in:
nise.moe 2024-03-09 14:33:12 +01:00
parent 6a9b14e7e9
commit 482079fb8f
15 changed files with 207 additions and 143 deletions

View File

@ -40,6 +40,7 @@ class FollowsController(
@GetMapping("follows")
fun getFollowsBanStatus(): ResponseEntity<FollowsBanStatusResponse> {
authService.getCurrentUser().userId
val follows = dslContext.select(
USERS.USER_ID,
USERS.USERNAME,

View File

@ -6,6 +6,7 @@ import org.springframework.security.authentication.AnonymousAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.oauth2.core.user.DefaultOAuth2User
import org.springframework.stereotype.Service
import java.time.OffsetDateTime
@Service
class AuthService(
@ -14,7 +15,11 @@ class AuthService(
data class UserInfo(
val userId: Long,
val username: String
val username: String,
val country: String?,
val joinDate: OffsetDateTime?,
)
fun isAdmin(): Boolean {
@ -39,7 +44,9 @@ class AuthService(
return UserInfo(
userId = (userDetails.attributes["id"] as Int).toLong(),
username = userDetails.attributes["username"] as String
username = userDetails.attributes["username"] as String,
country = userDetails.attributes["countryCode"] as String?,
joinDate = OffsetDateTime.parse(userDetails.attributes["joinDate"] as String)
)
}

View File

@ -22,7 +22,7 @@ RUN sed -i '238s|return \[x for x in arr if lower_limit < x < upper_limit\]|arr_
COPY ./src/ ./src/
ENV GUNICORN_CMD_ARGS="--bind=0.0.0.0:5000 --workers=10"
ENV GUNICORN_CMD_ARGS="--bind=0.0.0.0:5000 --workers=16"
# Run gunicorn with the application
CMD ["gunicorn", "--chdir", "src", "main:app"]

View File

@ -9,7 +9,7 @@ import {ViewReplayPairComponent} from "./view-replay-pair/view-replay-pair.compo
import {SearchComponent} from "./search/search.component";
import {ContributeComponent} from "./contribute/contribute.component";
import {BanlistComponent} from "./banlist/banlist.component";
import {FollowsComponent} from "./follows/follows.component";
import {ProfileComponent} from "./profile/profile.component";
const routes: Routes = [
{path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'},
@ -25,7 +25,7 @@ const routes: Routes = [
{path: 'p/:replay1Id/:replay2Id', component: ViewReplayPairComponent},
{path: 'follows', component: FollowsComponent, title: '/follows/'},
{path: 'profile', component: ProfileComponent},
{path: 'banlist', component: BanlistComponent, title: '/ban/'},
{path: 'contribute', component: ContributeComponent, title: '/contribute/ <3'},

View File

@ -15,3 +15,10 @@
.link-pink:hover {
color: rgba(234, 78, 179, 0.97)
}
.logout-btn {
padding: 1px 2px 2px 1px;
font-size: 14px;
font-weight: bold;
border: 1px dashed rgba(151, 151, 151, 0.97);
}

View File

@ -18,7 +18,7 @@
</form>
<div style="margin-top: 3px">
<ng-container *ngIf="this.userService.isUserLoggedIn()">
hi, <span class="user-details" [title]="this.userService.currentUser?.username">{{this.userService.currentUser?.username}}</span> <a [href]="this.userService.getLogoutUrl()">Logout</a>
hi, <span class="user-details" [title]="this.userService.currentUser?.username"><a [routerLink]="['/profile']"> {{this.userService.currentUser?.username}}</a></span> <a class="logout-btn" [href]="this.userService.getLogoutUrl()">Logout</a>
</ng-container>
<ng-container *ngIf="!this.userService.isUserLoggedIn()">
<a [href]="this.userService.getLoginUrl()">Login</a>
@ -26,6 +26,13 @@
</div>
</div>
</div>
<div class="main term text-center statistics">
Players: {{ this.statistics?.total_users | number }} | Beatmaps: {{ this.statistics?.total_beatmaps | number }} | Scores: {{ this.statistics?.total_scores | number }}
<ng-container *ngIf="this.statistics?.total_bans">
| <a [routerLink]="['/banlist']">Bans: {{ this.statistics?.total_bans | number }}</a>
</ng-container>
</div>
<router-outlet></router-outlet>
<div class="text-center version">
v20240309

View File

@ -1,28 +1,57 @@
import {Component} from '@angular/core';
import {Component, OnInit} from '@angular/core';
import {Router, RouterLink, RouterOutlet} from "@angular/router";
import {UserService} from "../corelib/service/user.service";
import {NgIf} from '@angular/common';
import {DecimalPipe, NgIf} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {Observable} from "rxjs";
import {environment} from "../environments/environment";
import {LocalCacheService} from "../corelib/service/local-cache.service";
interface Statistics {
total_beatmaps: number;
total_users: number;
total_scores: number;
total_replay_scores: number;
total_replay_similarity: number;
total_bans: number;
}
@Component({
selector: 'app-root',
standalone: true,
imports: [
RouterLink,
FormsModule,
NgIf,
RouterOutlet
],
imports: [
RouterLink,
FormsModule,
NgIf,
RouterOutlet,
DecimalPipe
],
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
export class AppComponent implements OnInit {
statistics: Statistics | null = null;
term: string = '';
constructor(private router: Router,
private localCacheService: LocalCacheService,
public userService: UserService
) {
) { }
ngOnInit(): void {
this.getStatistics().subscribe((response: Statistics) => {
this.statistics = response;
});
}
getStatistics(): Observable<Statistics> {
return this.localCacheService.fetchData<Statistics>(
'statistics',
`${environment.apiUrl}/stats`,
60
);
}
onSubmit(): void {

View File

@ -21,55 +21,83 @@
<ng-template #nullTemplate>
<code>null</code>
</ng-template>
<table *ngIf="this.banlist">
<thead>
<tr>
<th colspan="2">Username</th>
<th>Time played</th>
<th>Total PP</th>
<th>Rank</th>
<th>Last check</th>
<th>Approximate ban date</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of this.banlist.users">
<td>
<img [src]="'https://a.ppy.sh/' + user.userId" class="avatar" style="width: 16px; min-height: 16px; height: 16px;" loading="lazy">
</td>
<td>
<a [routerLink]="['/u', user.username]">
{{ user.username }}
</a>
</td>
<td>
<ng-container *ngIf="user.secondsPlayed else nullTemplate">
{{ formatDuration(user.secondsPlayed) }}
</ng-container>
</td>
<td>
<ng-container *ngIf="user.pp; else nullTemplate">
{{ user.pp | number: '1.0-0' }}
</ng-container>
</td>
<td>
<ng-container *ngIf="user.rank; else nullTemplate">
#{{ user.rank | number }}
</ng-container>
</td>
<td>
<ng-container *ngIf="user.lastUpdate; else nullTemplate">
{{ calculateTimeAgo(user.lastUpdate) }}
</ng-container>
</td>
<td>
{{ user.approximateBanTime | date: 'medium' }}
</td>
<td>
</td>
</tbody>
</table>
<ng-container *ngIf="this.banlist">
<fieldset class="mb-2">
<legend>tools</legend>
<div class="text-center">
<button (click)="this.downloadFilesService.downloadCSV(this.banlist.users, ['userId', 'username', 'secondsPlayed', 'pp', 'rank', 'isBanned', 'approximateBanTime', 'lastUpdate'], 'nise-banlist')">Download .csv</button>
<button (click)="this.downloadFilesService.downloadJSON(this.banlist.users, 'nise-banlist')">Download .json</button>
<button (click)="this.downloadFilesService.downloadXLSX(this.banlist.users, 'nise-banlist')">Download .xlsx</button>
</div>
</fieldset>
<table>
<thead>
<tr>
<th colspan="2">Username</th>
<th>Time played</th>
<th>Total PP</th>
<th>Rank</th>
<th>Last check</th>
<th>Approximate ban date</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of this.banlist.users">
<td>
<img [src]="'https://a.ppy.sh/' + user.userId" class="avatar" style="width: 16px; min-height: 16px; height: 16px;" loading="lazy">
</td>
<td>
<a [routerLink]="['/u', user.username]">
{{ user.username }}
</a>
</td>
<td>
<ng-container *ngIf="user.secondsPlayed else nullTemplate">
{{ formatDuration(user.secondsPlayed) }}
</ng-container>
</td>
<td>
<ng-container *ngIf="user.pp; else nullTemplate">
{{ user.pp | number: '1.0-0' }}
</ng-container>
</td>
<td>
<ng-container *ngIf="user.rank; else nullTemplate">
#{{ user.rank | number }}
</ng-container>
</td>
<td>
<ng-container *ngIf="user.lastUpdate; else nullTemplate">
{{ calculateTimeAgo(user.lastUpdate) }}
</ng-container>
</td>
<td>
{{ user.approximateBanTime | date: 'medium' }}
</td>
<td>
</td>
</tbody>
</table>
<div class="text-center mt-2">
<p>Total results: {{ this.banlist.pagination.totalResults | number }}</p>
<p>Page: {{ this.banlist.pagination.currentPage | number }} / {{ this.banlist.pagination.totalPages | number }}</p>
<div class="mb-2">
<button *ngIf="this.banlist.pagination.currentPage > 5" (click)="this.getBanlist(1)" style="margin-right: 5px">1</button>
<span *ngIf="this.banlist.pagination.currentPage > 6">... </span>
<button *ngFor="let page of [].constructor(Math.min(this.banlist.pagination.totalPages, 10)) | calculatePageRange:this.banlist.pagination.currentPage:this.banlist.pagination.totalPages; let i = index"
(click)="this.getBanlist(page)"
[disabled]="page == this.banlist.pagination.currentPage"
style="margin-right: 5px">
{{ page }}
</button>
<span *ngIf="this.banlist.pagination.currentPage < this.banlist.pagination.totalPages - 5">... </span>
<button *ngIf="this.banlist.pagination.currentPage < this.banlist.pagination.totalPages - 4" (click)="this.getBanlist(this.banlist.pagination.totalPages)" style="margin-right: 5px">{{ this.banlist.pagination.totalPages }}</button>
</div>
<button (click)="this.getBanlist(this.banlist.pagination.currentPage - 1)" [disabled]="this.banlist.pagination.currentPage == 1">← Previous</button>
<button (click)="this.getBanlist(this.banlist.pagination.currentPage + 1)" [disabled]="this.banlist.pagination.currentPage == this.banlist.pagination.totalPages" style="margin-left: 5px">Next →</button>
</div>
</ng-container>
</div>
</div>

View File

@ -6,6 +6,8 @@ import {calculateTimeAgo, formatDuration} from "../format";
import {RouterLink} from "@angular/router";
import {CuteProgressbarComponent} from "../../corelib/components/cute-progressbar/cute-progressbar.component";
import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component";
import {DownloadFilesService} from "../../corelib/service/download-files.service";
import {CalculatePageRangePipe} from "../../corelib/calculate-page-range.pipe";
interface BanlistResponse {
pagination: BanlistPagination;
@ -41,7 +43,8 @@ interface BanlistEntry {
DatePipe,
DecimalPipe,
CuteProgressbarComponent,
CuteLoadingComponent
CuteLoadingComponent,
CalculatePageRangePipe
],
templateUrl: './banlist.component.html',
styleUrl: './banlist.component.css'
@ -51,15 +54,18 @@ export class BanlistComponent implements OnInit {
isLoading = true;
banlist: BanlistResponse | null = null;
constructor(private httpClient: HttpClient) { }
constructor(
private httpClient: HttpClient,
public downloadFilesService: DownloadFilesService
) { }
ngOnInit(): void {
this.getBanlist();
}
getBanlist(): void {
getBanlist(page: number = 1): void {
const body = {
page: 1
page: page
}
this.httpClient.post<BanlistResponse>(`${environment.apiUrl}/banlist`, body).subscribe(response => {
this.banlist = response;
@ -69,4 +75,5 @@ export class BanlistComponent implements OnInit {
protected readonly calculateTimeAgo = calculateTimeAgo;
protected readonly formatDuration = formatDuration;
protected readonly Math = Math;
}

View File

@ -1,39 +1,32 @@
<div class="main container">
<div class="subcontainer" style="width: 100%">
<div class="term" style="width: 100%">
<div style="overflow: hidden;">
<img src="assets/contribute.png" width="184" style="float: left; margin-right: 20px;">
<h1># contributing to the project</h1>
<p>if you'd like to keep the project healthy and funded in the long term, you are invited to directly contribute.</p>
<p>the website does not run ads, paywall features, or collect your private data in any way. i have no intention to change that since I want to provide the best possible service to the osu! community.</p>
<p>still, hosting a (relatively) computationally expensive service has costs that are simply unavoidable. that's why I'd encourage people that find this website useful to contribute:</p>
<div class="text-center mt-2 mb-2">
<a href="https://patreon.com/nise_moe" target="_blank" class="btn btn-patreon">Become a Patreon</a>
</div>
<p>the money will be used to pay for the server hosting and development time.</p>
<table style="display: block;">
<tbody>
<tr>
<td style="font-weight: bold">server hosting</td>
<td style="padding-left: 40px">~220$/year</td>
</tr>
<tr>
<td style="font-weight: bold">domain</td>
<td style="padding-left: 40px">~17$/year</td>
</tr>
<hr>
<tr>
<td style="font-weight: bold">total</td>
<td style="padding-left: 40px">~237$/year</td>
</tr>
</tbody>
</table>
</div>
<div class="main term" style="width: 100%">
<div style="overflow: hidden;">
<img src="assets/contribute.png" width="184" style="float: left; margin-right: 20px;">
<h1># contributing to the project</h1>
<p>if you'd like to keep the project healthy and funded in the long term, you are invited to directly contribute.</p>
<p>the website does not run ads, paywall features, or collect your private data in any way. i have no intention to change that since I want to provide the best possible service to the osu! community.</p>
<p>still, hosting a (relatively) computationally expensive service has costs that are simply unavoidable. that's why I'd encourage people that find this website useful to contribute:</p>
<div class="text-center mt-2 mb-2">
<a href="https://patreon.com/nise_moe" target="_blank" class="btn btn-patreon">Become a Patreon</a>
</div>
<p>the money will be used to pay for the server hosting and development costs.</p>
<table style="display: block;">
<tbody>
<tr>
<td style="font-weight: bold">server hosting</td>
<td style="padding-left: 40px">~220$/year</td>
</tr>
<tr>
<td style="font-weight: bold">domain</td>
<td style="padding-left: 40px">~17$/year</td>
</tr>
<hr>
<tr>
<td style="font-weight: bold">total</td>
<td style="padding-left: 40px">~237$/year</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -1,11 +1,4 @@
<div class="main term text-center statistics">
Players: {{ this.statistics?.total_users | number }} | Beatmaps: {{ this.statistics?.total_beatmaps | number }} | Scores: {{ this.statistics?.total_scores | number }}
<ng-container *ngIf="this.statistics?.total_bans">
| <a [routerLink]="['/banlist']">Bans: {{ this.statistics?.total_bans | number }}</a>
</ng-container>
</div>
<div class="main container">
<div class="main container" style="width: 886px !important;">
<div class="subcontainer">
<div class="term">

View File

@ -1,24 +1,14 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {Observable, Subscription} from "rxjs";
import {Subscription} from "rxjs";
import {environment} from "../../environments/environment";
import {LocalCacheService} from "../../corelib/service/local-cache.service";
import {RxStompService} from "../../corelib/stomp/stomp.service";
import {Message} from "@stomp/stompjs/esm6";
import {ErrorDistribution, ReplayData, ReplayDataChart, ReplayDataSimilarScore} from "../replays";
import {ReplayData} from "../replays";
import {DecimalPipe, NgForOf, NgIf} from "@angular/common";
import {Router, RouterLink} from "@angular/router";
import {HttpClient} from "@angular/common/http";
import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component";
interface Statistics {
total_beatmaps: number;
total_users: number;
total_scores: number;
total_replay_scores: number;
total_replay_similarity: number;
total_bans: number;
}
interface AnalyzeReplayResponse {
id: string;
}
@ -41,13 +31,11 @@ export class HomeComponent implements OnInit, OnDestroy {
liveScores: ReplayData[] = [];
liveScoresSub: Subscription | undefined;
statistics: Statistics | null = null;
wantsConnection: boolean = true;
loading = false;
constructor(
private localCacheService: LocalCacheService,
private router: Router,
private httpClient: HttpClient,
private rxStompService: RxStompService,
@ -63,10 +51,6 @@ export class HomeComponent implements OnInit, OnDestroy {
} else {
this.subscribe();
}
this.getStatistics().subscribe((response: Statistics) => {
this.statistics = response;
});
}
private subscribe() {
@ -96,14 +80,6 @@ export class HomeComponent implements OnInit, OnDestroy {
}
}
getStatistics(): Observable<Statistics> {
return this.localCacheService.fetchData<Statistics>(
'statistics',
`${environment.apiUrl}/stats`,
60
);
}
uploadReplay(event: any) {
if (event.target.files.length <= 0) {
return;

View File

@ -1,7 +1,16 @@
<div class="main term mb-2">
<div class="fade-stuff">
<h1 class="mb-4"><span class="text-muted">hi,</span> {{ this.userService.currentUser?.username }}</h1>
<h1 class="mb-4"># follow-list</h1>
<table *ngIf="this.follows">
<ng-template #noFollows>
<div class="text-center">
<p>You are not following anyone!</p>
<p>You can follow users by going on their profile and clicking the <code>(+) Add follow</code> buttan.</p>
<p>Then, they'll appear here, and you'll be able to check if they've been banned or not.</p>
</div>
</ng-template>
<table *ngIf="this.follows && this.follows.follows.length > 0; else noFollows">
<thead>
<tr>
<th colspan="2">Username</th>

View File

@ -4,6 +4,8 @@ import {environment} from "../../environments/environment";
import {JsonPipe, NgForOf, NgIf} from "@angular/common";
import {calculateTimeAgo} from "../format";
import {RouterLink} from "@angular/router";
import {UserService} from "../../corelib/service/user.service";
import {Title} from "@angular/platform-browser";
interface FollowsBanStatusResponse {
follows: FollowsBanStatusEntry[];
@ -18,7 +20,7 @@ interface FollowsBanStatusEntry {
@Component({
selector: 'app-follows',
selector: 'app-profile',
standalone: true,
imports: [
JsonPipe,
@ -26,16 +28,21 @@ interface FollowsBanStatusEntry {
NgIf,
RouterLink
],
templateUrl: './follows.component.html',
styleUrl: './follows.component.css'
templateUrl: './profile.component.html',
styleUrl: './profile.component.css'
})
export class FollowsComponent implements OnInit {
export class ProfileComponent implements OnInit {
follows: FollowsBanStatusResponse | null = null;
constructor(private httpClient: HttpClient) { }
constructor(
private httpClient: HttpClient,
private title: Title,
public userService: UserService
) { }
ngOnInit(): void {
this.title.setTitle(`hi, ${this.userService.currentUser!.username}`);
this.getFollows();
}