Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions src/app/api/types/levels/level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ export interface Level {
totalPlays: number;
uniquePlays: number;
publisher: User | undefined;
originalPublisher: string | undefined;
isReUpload: boolean;
isModded: boolean;
teamPicked: boolean;
dateTeamPicked: Date | undefined;
gameVersion: number;
score: number;
slotType: 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
43 changes: 43 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,43 @@
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';

@Component({
selector: 'app-level-team-pick-status',
imports: [
FaIconComponent,
TooltipComponent
],
template: `
<app-tooltip [text]="(short ? 'Team picked since ' : 'Since ') + this.formattedTime">
<!-- TODO: Use "primary" once that is a declared color instead of "yellow" to prepare for themes -->
<div class="flex flex-row gap-x-1 text-yellow">
<fa-icon [icon]="faCircleCheck"></fa-icon>
@if (!short) {
<p>team picked {{this.shortTime}}</p>
}

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

protected formattedTime: string = "unknown";
protected shortTime: string = "";

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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {faEllipsisV} from "@fortawesome/free-solid-svg-icons";
</app-button>
</ng-template>

<div class="flex flex-row justify-end content-center space-x-1 min-w-56 group relative">
<div class="flex flex-row justify-end content-center space-x-1 group relative text-nowrap">
<div #firstButtonContainer></div>
<div #secondButtonContainer></div>
<div class="absolute z-1 flex flex-col gap-y-1.5 w-48 px-5 py-2.5 rounded bg-header-background
Expand Down
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>uploaded 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