Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
06afa35
Show level team pick status
Toastbrot236 Nov 29, 2025
51f12d7
Refactor formatting out of DateComponent, unify team pick tooltips
Toastbrot236 Nov 29, 2025
f7ad181
Refactor team pick status into an own component
Toastbrot236 Nov 29, 2025
a532ccf
Stuff I forgot to remove after previous refactor
Toastbrot236 Nov 29, 2025
c52c0ca
Show level reupload data
Toastbrot236 Nov 29, 2025
14d10db
Forematting
Toastbrot236 Nov 29, 2025
a55d52a
Improve reupload username styling
Toastbrot236 Nov 29, 2025
b59ac14
Split date formatting methods further
Toastbrot236 Nov 30, 2025
ce9bd25
Highlight team pick info if team pick was less than a month ago
Toastbrot236 Nov 30, 2025
dbc28f0
Simplify showing publisher on level page at the cost of not showing t…
Toastbrot236 Nov 30, 2025
cf78c87
Expose modded status, slightly reorder level header to match a certai…
Toastbrot236 Nov 30, 2025
27f490e
Shorten team pick hint on level preview, always keep it yellow, other…
Toastbrot236 Nov 30, 2025
7ebc102
The forgotten removals
Toastbrot236 Nov 30, 2025
2d1df03
Optimize team pick date formatting
Toastbrot236 Nov 30, 2025
0bf7d47
Make publisher string clearer for reuploads + improve wrapping, don't…
Toastbrot236 Dec 1, 2025
259f920
Merge branch 'main' into reupload
Toastbrot236 Feb 6, 2026
53272da
Revert setting Level.dateTeamPicked's type to Date | undefined
Toastbrot236 Feb 6, 2026
119b0c4
Shorten team pick notice for mobile widths
Toastbrot236 Feb 7, 2026
17f7eb2
Add and use emphasized-primary as a color
Toastbrot236 Feb 7, 2026
a4a7b3a
Slightly better wording
Toastbrot236 Feb 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/app/api/types/levels/level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Level {
publisher: User | undefined;
originalPublisher: string | undefined;
isReUpload: boolean;
isModded: boolean;
teamPicked: boolean;
dateTeamPicked: Date;
gameVersion: number;
Expand Down
45 changes: 32 additions & 13 deletions src/app/components/items/level-preview.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@ import {GamePipe} from "../../pipes/game.pipe";
import {DateComponent} from "../ui/info/date.component";
import {DefaultPipe} from "../../pipes/default.pipe";
import {LabelComponent} from "../ui/info/label.component";
import { OriginalPublisherRouterLink } from "../ui/text/links/original-publisher-router-link.component";
import { TooltipComponent } from "../ui/text/tooltip.component";

@Component({
selector: 'app-level-preview',
imports: [
UserLinkComponent,
LevelAvatarComponent,
LevelStatisticsComponent,
LevelRouterLinkComponent,
GamePipe,
DateComponent,
DefaultPipe,
LabelComponent
],
UserLinkComponent,
LevelAvatarComponent,
LevelStatisticsComponent,
LevelRouterLinkComponent,
GamePipe,
DateComponent,
DefaultPipe,
LabelComponent,
OriginalPublisherRouterLink,
TooltipComponent
],
template: `
<div class="flex gap-x-2.5 leading-none justify-center">
<app-level-router-link [level]="level" class="min-w-[72px] self-center">
Expand All @@ -32,15 +36,30 @@ import {LabelComponent} from "../ui/info/label.component";
[title]=level.title>{{ level.title | default: "Unnamed Level" }}</p>
</app-level-router-link>

<app-level-statistics [level]="level" class="text-sm"></app-level-statistics>
<app-level-statistics [level]="level" [short]="true" class="text-sm"></app-level-statistics>

<div class="text-gentle text-sm mt-0.5 flex gap-x-1">
by <app-user-link [user]="level.publisher"></app-user-link>
<div class="text-gentle text-sm mt-0.5 flex flex-wrap gap-x-1">
@if (level.isReUpload) {
<p>by <app-original-publisher-router-link [level]="this.level"></app-original-publisher-router-link></p>
<p>(<app-user-link [user]="level.publisher"></app-user-link>)</p>
}
@else {
<p>by <app-user-link [user]="level.publisher"></app-user-link></p>
}

<app-date [date]="level.publishDate"></app-date>
</div>

<div class="flex gap-x-1 mt-1">
<app-label [primary]="true">{{level.gameVersion | game: true}}</app-label>
<app-tooltip [text]="'This level was published in ' + (level.gameVersion | game: false)">
<app-label [primary]="true">{{level.gameVersion | game: true}}</app-label>
</app-tooltip>

@if (level.isModded) {
<app-tooltip text="This level contains modded assets">
<app-label [primary]="true">Modded</app-label>
</app-tooltip>
}
<!-- <app-label>Platformer</app-label>-->
<!-- <app-label>Short</app-label>-->
<!-- <app-label>Insane</app-label>-->
Expand Down
14 changes: 11 additions & 3 deletions src/app/components/items/level-statistics.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,32 @@ import {Component, Input} from '@angular/core';
import {Level} from "../../api/types/levels/level";
import {faHeart, faPlay, faStar, faThumbsDown, faThumbsUp} from "@fortawesome/free-solid-svg-icons";
import {StatisticComponent} from "../ui/info/statistic.component";
import { getFormattedDateTime, getShortDateTime } from '../../helpers/date-time';
import { LevelTeamPickStatusComponent } from "./level-team-pick-status.component";

@Component({
selector: 'app-level-statistics',
imports: [
StatisticComponent
],
StatisticComponent,
LevelTeamPickStatusComponent
],
template: `
<div class="flex gap-x-1.5">
<div class="flex flex-wrap gap-x-1.5">
<app-statistic [value]=level.yayRatings name="Yays" [icon]=faThumbsUp></app-statistic>
<app-statistic [value]=level.booRatings name="Boos" [icon]=faThumbsDown></app-statistic>
<app-statistic [value]=level.hearts name="Hearts" [icon]=faHeart></app-statistic>
<app-statistic [value]=level.uniquePlays name="Plays" [icon]=faPlay></app-statistic>
<app-statistic [value]=level.score name="Cool Rating (CR)" [icon]=faStar [truncate]=true></app-statistic>
@if (level.teamPicked) {
<app-level-team-pick-status [level]="level" [short]="short"></app-level-team-pick-status>
}
</div>
`
})
export class LevelStatisticsComponent {
@Input({required: true}) level: Level = undefined!;
@Input() short: boolean = false;

protected readonly faThumbsUp = faThumbsUp;
protected readonly faThumbsDown = faThumbsDown;
protected readonly faHeart = faHeart;
Expand Down
52 changes: 52 additions & 0 deletions src/app/components/items/level-team-pick-status.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {Component, Input} from '@angular/core';
import {Level} from "../../api/types/levels/level";
import {faCircleCheck} from "@fortawesome/free-solid-svg-icons";
import { FaIconComponent } from "@fortawesome/angular-fontawesome";
import { TooltipComponent } from "../ui/text/tooltip.component";
import { getFormattedDateTime, getShortDateTime } from '../../helpers/date-time';
import { LayoutService } from '../../services/layout.service';

@Component({
selector: 'app-level-team-pick-status',
imports: [
FaIconComponent,
TooltipComponent
],
template: `
<app-tooltip [text]="(short ? 'Team picked since ' : 'Since ') + this.formattedTime">
<div class="flex flex-row gap-x-1 text-emphasized-primary">
<fa-icon [icon]="faCircleCheck"></fa-icon>

@if (!short) {
@if (!isMobile) {
<span>team picked </span>
}

<span>{{this.shortTime}}</span>
}
</div>
</app-tooltip>
`
})
export class LevelTeamPickStatusComponent {
@Input({required: true}) level: Level = undefined!;
@Input() short: boolean = false;

protected formattedTime: string = "unknown";
protected shortTime: string = "";
protected isMobile: boolean = false;

constructor(protected layout: LayoutService) {
this.layout.isMobile.subscribe(v => this.isMobile = v);
}

ngOnInit() {
if (this.level.dateTeamPicked != null) {
this.level.dateTeamPicked = new Date(this.level.dateTeamPicked);
this.shortTime = getShortDateTime(this.level.dateTeamPicked);
this.formattedTime = getFormattedDateTime(this.level.dateTeamPicked);
}
}

protected readonly faCircleCheck = faCircleCheck;
}
32 changes: 3 additions & 29 deletions src/app/components/ui/info/date.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '@angular/core';
import {TooltipComponent} from "../text/tooltip.component";
import {isPlatformBrowser} from "@angular/common";
import { getFormattedDateTime, getShortDateTime } from '../../../helpers/date-time';

@Component({
selector: 'app-date',
Expand Down Expand Up @@ -44,8 +45,6 @@ export class DateComponent implements OnInit, OnDestroy {
return isPlatformBrowser(this.platformId);
}

protected recentText = "just now";

// this setter is actually required to be here; this is not a hold-over from the old site
// for some reason, javascript's date parser doesn't create a full Date object.
// so we need to do this horse-shit.
Expand All @@ -55,36 +54,11 @@ export class DateComponent implements OnInit, OnDestroy {
}

get moment(): string {
const now = new Date();
const totalSeconds = Math.floor((now.getTime() - this._date.getTime()) / 1000);

const intervals: { [key: string]: number } = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1,
};

if (totalSeconds < 20)
return this.recentText;

for (const interval in intervals) {
const time = Math.floor(totalSeconds / intervals[interval]);
if (time > 1) {
return `${time} ${interval}s ago`;
} else if (time == 1) {
return `a${interval == "hour" ? 'n' : ''} ${interval} ago`;
}
}

return this.recentText;
return getShortDateTime(this._date);
}

get formattedDate(): string {
return `${this._date.toLocaleDateString()} @ ${this._date.toLocaleTimeString()}`;
return getFormattedDateTime(this._date);
}

ngOnDestroy() {
Expand Down
17 changes: 17 additions & 0 deletions src/app/components/ui/info/larger-label.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Component, Input} from '@angular/core';
import {NgClass} from "@angular/common";

@Component({
selector: 'app-larger-label',
imports: [
NgClass
],
template: `
<div [ngClass]="primary ? 'bg-primary' : 'bg-secondary'" class="py-1 px-2.5 rounded-md text-center text-[14px]">
<ng-content></ng-content>
</div>
`
})
export class LargerLabelComponent {
@Input() primary: boolean = false;
}
16 changes: 12 additions & 4 deletions src/app/components/ui/layouts/fancy-header.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,24 @@ import { NgTemplateOutlet } from "@angular/common";
</app-page-title>
<ng-content select="[buttonArea]"></ng-content>
</div>
<div>
<ng-content select="[belowTitle]"></ng-content>
<ng-content select="[statistics]"></ng-content>
<div class="flex flex-row gap-x-1 justify-between">
<div>
<ng-content select="[belowTitle]"></ng-content>
<ng-content select="[statistics]"></ng-content>
<ng-content select="[belowStatistics]"></ng-content>
</div>
<div>
<ng-content select="[belowTitleRight]"></ng-content>
</div>
</div>
</div>
</div>
<ng-content select="[buttonAreaMobile]"></ng-content>
<div class="mt-2.5">
<ng-container *ngTemplateOutlet="descriptionTemplate"></ng-container>
</div>
<div class="mt-2.5">
<ng-content select="[buttonAreaMobile]"></ng-content>
</div>
</app-container-header>
`
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {Component, Input} from '@angular/core';
import {RouterLink} from "@angular/router";
import { Level } from '../../../../api/types/levels/level';

@Component({
selector: 'app-original-publisher-router-link',
imports: [
RouterLink
],
template: `
<span class="italic">
<a routerLink="/user/!{{level.originalPublisher ?? this.unknownName}}" class="hover:underline"
>{{level.originalPublisher ?? this.unknownName}}</a>
</span>

`
})
export class OriginalPublisherRouterLink {
@Input({required: true}) public level: Level = undefined!;
protected unknownName: string = "Unknown";
}
33 changes: 33 additions & 0 deletions src/app/helpers/date-time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export function getFormattedDateTime(date: Date) {
return `${date.toLocaleDateString()} @ ${date.toLocaleTimeString()}`;
}

export function getShortDateTime(date: Date) {
const recentText = "just now";
const now = new Date();
const totalSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);

if (totalSeconds < 20)
return recentText;

for (const interval in timeIntervals) {
const time = Math.floor(totalSeconds / timeIntervals[interval]);
if (time > 1) {
return `${time} ${interval}s ago`;
} else if (time == 1) {
return `a${interval == "hour" ? 'n' : ''} ${interval} ago`;
}
}

return recentText;
}

export const timeIntervals: { [key: string]: number } = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1,
};
55 changes: 46 additions & 9 deletions src/app/pages/level/level.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,55 @@
<app-fancy-header [title]="level.title | default: 'Unnamed Level'" [description]="level.description | default: 'This level doesn\'t have a description.'">
<app-level-avatar [level]="level" [size]="(layout.isMobile | async) ? 90 : 176" avatar></app-level-avatar>
<ng-container titleSubtext>
<span class="text-nowrap">
by <app-user-link [user]="level.publisher"></app-user-link>
</span>
@if (level.isReUpload) {
<div class="flex flex-row flex-wrap gap-x-1 text-nowrap text-md">
<span>originally by <app-original-publisher-router-link [level]="this.level"></app-original-publisher-router-link>, </span>
<span>reuploaded by <app-user-link [user]="level.publisher"></app-user-link></span>
</div>
}
@else {
<p class="text-nowrap">
by <app-user-link [user]="level.publisher"></app-user-link>
</p>
}
</ng-container>
<div class="text-gentle sm:text-sm md:text-sm text-md" belowTitle>
Published for {{level.gameVersion | game: isMobile}}
<app-date [date]="level.publishDate"></app-date>
@if(level.updateDate) {,
updated
<app-date [date]="level.updateDate"></app-date>

<!-- Various non-label properties, shown as labels -->
<div belowStatistics class="flex flex-row flex-wrap my-1 gap-x-2 gap-y-1">
<app-tooltip [text]="'This level was published in ' + (level.gameVersion | game: false)">
<app-larger-label [primary]="true">{{level.gameVersion | game: isMobile}}</app-larger-label>
</app-tooltip>

@if (level.isModded) {
<app-tooltip text="This level contains modded assets">
<app-larger-label [primary]="true">Modded</app-larger-label>
</app-tooltip>
}
</div>

@if (!isMobile) {
<div belowTitleRight class="flex flex-col flex-grow text-gentle italic sm:text-sm md:text-sm text-md mr-1">
<div class="flex flex-row gap-x-1 text-nowrap justify-end">
<p>published</p><app-date [date]="level.publishDate"></app-date>
</div>

@if(level.updateDate != level.publishDate) {
<div class="flex flex-row gap-x-1 text-nowrap justify-end">
<p>updated</p><app-date [date]="level.updateDate"></app-date>
</div>
}
</div>
}
@else {
<div belowTitle class="text-gentle italic">
published <app-date [date]="level.publishDate"></app-date>

@if(level.updateDate != level.publishDate) {,
updated <app-date [date]="level.updateDate"></app-date>
}
</div>
}

<app-level-statistics [level]="level" class="mb-1.5 block" statistics></app-level-statistics>
@if(relations && !isMobile) {
<app-fancy-header-level-buttons [level]="level" [ownUser]="ownUser!" [relations]="relations" buttonArea></app-fancy-header-level-buttons>
Expand Down
Loading