Skip to content

Commit

Permalink
Merge branch 'develop' into leaderboard-pr-count
Browse files Browse the repository at this point in the history
  • Loading branch information
FelixTJDietrich authored Oct 1, 2024
2 parents b896ad7 + 2bf309a commit 78116dd
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 11 deletions.
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() && !query.data()) {
<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 { combineLatest, timer, lastValueFrom, map } 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(combineLatest([this.leaderboardService.getLeaderboard(this.after(), this.before()), timer(500)]).pipe(map(([leaderboard]) => leaderboard)))
}));
}
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" />`
})
};

0 comments on commit 78116dd

Please sign in to comment.