Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GUI for Custom Time Sliding Window in Leaderboard #105

Merged
merged 8 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
23 changes: 15 additions & 8 deletions webapp/src/app/home/home.component.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
<div class="flex gap-2 flex-col items-center justify-center">
<h1 class="text-3xl font-bold mb-4">Artemis Leaderboard</h1>
@if (query.isPending()) {
<span class="text-muted-foreground">Data is loading...</span>
} @else if (query.error()) {
<span class="text-destructive">An error has occurred</span>
}
@if (query.data()) {
<app-leaderboard [leaderboard]="query.data()" />
}

<div class="grid grid-cols-1 md:grid-cols-3 gap-4 place-content-center">
<app-leaderboard-filter [after]="after()" [before]="before()" />
<div class="md:col-span-2 flex flex-col items-center gap-4">
@if (query.isPending()) {
<app-skeleton class="w-full h-96"></app-skeleton>
} @else if (query.error()) {
<span class="text-xl text-destructive mt-2">An error has occurred</span>
} @else if (query.data()) {
<div class="border rounded-md border-input">
<app-leaderboard [leaderboard]="query.data()" />
</div>
}
</div>
</div>
</div>
8 changes: 5 additions & 3 deletions webapp/src/app/home/home.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { ActivatedRoute } from '@angular/router';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { LeaderboardService } from 'app/core/modules/openapi/api/leaderboard.service';
import { LeaderboardComponent } from 'app/home/leaderboard/leaderboard.component';
import { lastValueFrom } from 'rxjs';
import { delay, lastValueFrom } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';
import { LeaderboardFilterComponent } from './leaderboard/filter/filter.component';
import { SkeletonComponent } from 'app/ui/skeleton/skeleton.component';

@Component({
selector: 'app-home',
standalone: true,
imports: [LeaderboardComponent],
imports: [LeaderboardComponent, LeaderboardFilterComponent, SkeletonComponent],
templateUrl: './home.component.html'
})
export class HomeComponent {
Expand All @@ -24,6 +26,6 @@ export class HomeComponent {

query = injectQuery(() => ({
queryKey: ['leaderboard', { after: this.after(), before: this.before() }],
queryFn: async () => lastValueFrom(this.leaderboardService.getLeaderboard(this.after(), this.before()))
queryFn: async () => lastValueFrom(this.leaderboardService.getLeaderboard(this.after(), this.before()).pipe(delay(1000)))
}));
}
10 changes: 10 additions & 0 deletions webapp/src/app/home/leaderboard/filter/filter.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div class="flex flex-col gap-2 border rounded-md border-input bg-background p-4">
<span class="flex items-center pb-2 gap-2 text-muted-foreground">
<ng-icon [svg]="octFilter" size="16"></ng-icon>
<h3 class="text-base font-semibold tracking-wide">Filter</h3>
</span>
<div class="flex flex-col items-start gap-1">
<app-label for="timeframe" class="text-sm font-normal text-muted-foreground leading-relaxed indent-1">Timeframe</app-label>
<app-select name="timeframe" [options]="options()" (selectChange)="selectFn($event)" [defaultOption]="after() + '.' + before()"></app-select>
</div>
</div>
55 changes: 55 additions & 0 deletions webapp/src/app/home/leaderboard/filter/filter.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Component, input, signal } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { SelectComponent, SelectOption } from 'app/ui/select/select.component';
import dayjs from 'dayjs';
import { LabelComponent } from '../../../ui/label/label.component';
import { octFilter } from '@ng-icons/octicons';
import { NgIconComponent } from '@ng-icons/core';

@Component({
selector: 'app-leaderboard-filter',
standalone: true,
imports: [SelectComponent, RouterLink, LabelComponent, NgIconComponent],
templateUrl: './filter.component.html'
})
export class LeaderboardFilterComponent {
protected octFilter = octFilter;
after = input<string>();
before = input<string>();

options = signal<SelectOption[]>([]);

constructor(private router: Router) {
// get monday - sunday of last 4 weeks
const options = new Array<SelectOption>();
const now = dayjs();
let currentDate = dayjs().day(1);
options.push({
id: now.unix(),
value: `${currentDate.format('YYYY-MM-DD')}.${now.format('YYYY-MM-DD')}`,
label: `${currentDate.format('MMM D')} - ${now.format('MMM D')}`
});
for (let i = 0; i < 4; i++) {
const startDate = currentDate.subtract(7, 'day');
const endDate = currentDate.subtract(1, 'day');
options.push({
id: startDate.unix(),
value: `${startDate.format('YYYY-MM-DD')}.${endDate.format('YYYY-MM-DD')}`,
label: `${startDate.format('MMM D')} - ${endDate.format('MMM D')}`
});
currentDate = startDate;
}
this.options.set(options);
}

selectFn(value: string) {
const dates = value.split('.');
// change query params
this.router.navigate([], {
queryParams: {
after: dates[0],
before: dates[1]
}
});
}
}
36 changes: 36 additions & 0 deletions webapp/src/app/home/leaderboard/filter/filter.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular';
import { LeaderboardFilterComponent } from './filter.component';

const meta: Meta<LeaderboardFilterComponent> = {
title: 'Components/Home/LeaderboardFilter',
component: LeaderboardFilterComponent,
tags: ['autodocs'],
argTypes: {
after: {
control: {
type: 'text'
},
description: 'Left limit of the timeframe'
},
before: {
control: {
type: 'text'
},
description: 'Right limit of the timeframe'
}
}
};

export default meta;
type Story = StoryObj<LeaderboardFilterComponent>;

export const Default: Story = {
args: {
after: '2024-09-09',
before: '2024-09-15'
},
render: (args) => ({
props: args,
template: `<app-leaderboard-filter ${argsToTemplate(args)} />`
})
};
9 changes: 9 additions & 0 deletions webapp/src/app/ui/select/select.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<select
class="h-10 w-full bg-background cursor-pointer select-none border-r-8 border-transparent px-4 text-base outline outline-1 outline-input rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
(change)="onSelectChange($event)"
[name]="name()"
>
@for (option of options(); track option.id) {
<option value="{{ option.value }}" [selected]="option.value === defaultOption()">{{ option.label }}</option>
}
</select>
24 changes: 24 additions & 0 deletions webapp/src/app/ui/select/select.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Component, input, output } from '@angular/core';

export type SelectOption = {
id: number;
value: string;
label: string;
};

@Component({
selector: 'app-select',
standalone: true,
templateUrl: './select.component.html'
})
export class SelectComponent {
options = input.required<SelectOption[]>();
defaultOption = input<string>();
selectChange = output<string>();
name = input<string>();

onSelectChange(event: Event) {
const value = (event.target as HTMLSelectElement).value;
this.selectChange.emit(value);
}
}
37 changes: 37 additions & 0 deletions webapp/src/app/ui/select/select.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular';
import { SelectComponent } from './select.component';

const meta: Meta<SelectComponent> = {
title: 'UI/Select',
component: SelectComponent,
tags: ['autodocs'],
args: {
options: [
{
id: 1,
value: 'option-1',
label: 'Option 1'
},
{
id: 2,
value: 'option-2',
label: 'Option 2'
},
{
id: 3,
value: 'option-3',
label: 'Option 3'
}
]
}
};

export default meta;
type Story = StoryObj<SelectComponent>;

export const Default: Story = {
render: (args) => ({
props: args,
template: `<app-select ${argsToTemplate(args)} />`
})
};
23 changes: 23 additions & 0 deletions webapp/src/app/ui/skeleton/skeleton.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Component, computed, input } from '@angular/core';
import { cn } from 'app/utils';
import { ClassValue } from 'clsx';

export type SelectOption = {
id: string;
value: string;
label: string;
};

@Component({
selector: 'app-skeleton',
standalone: true,
template: '',
host: {
'[class]': 'computedClass()'
}
})
export class SkeletonComponent {
class = input<ClassValue>('');

computedClass = computed(() => cn('block animate-pulse rounded-md bg-muted', this.class()));
}
18 changes: 18 additions & 0 deletions webapp/src/app/ui/skeleton/skeleton.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { type Meta, type StoryObj } from '@storybook/angular';
import { SkeletonComponent } from './skeleton.component';

const meta: Meta<SkeletonComponent> = {
title: 'UI/Skeleton',
component: SkeletonComponent,
tags: ['autodocs']
};

export default meta;
type Story = StoryObj<SkeletonComponent>;

export const Default: Story = {
render: (args) => ({
props: args,
template: `<app-skeleton class="size-12" />`
})
};