Skip to content

Commit

Permalink
Extract Header Component
Browse files Browse the repository at this point in the history
  • Loading branch information
GODrums committed Oct 17, 2024
1 parent 18c3597 commit d13510b
Show file tree
Hide file tree
Showing 12 changed files with 319 additions and 245 deletions.
49 changes: 49 additions & 0 deletions webapp/src/app/user/header/header.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<div class="flex gap-8 items-center justify-center mb-8">
@if (isLoading()) {
<hlm-avatar variant="extralarge" class="ring-2 ring-neutral-100 dark:ring-neutral-800">
<hlm-skeleton hlmAvatarImage class="h-full w-full rounded-full"></hlm-skeleton>
<hlm-skeleton hlmAvatarFallback class="h-full w-full rounded-full"></hlm-skeleton>
</hlm-avatar>
} @else {
<hlm-avatar variant="extralarge" class="ring-2 ring-neutral-100 dark:ring-neutral-800">
<img [src]="userData()?.avatarUrl" [alt]="userData()?.login + '\'s avatar'" hlmAvatarImage />
<span hlmAvatarFallback>
{{ userData()?.login?.slice(0, 2)?.toUpperCase() }}
</span>
</hlm-avatar>
}
<div class="flex flex-col gap-2">
@if (isLoading()) {
<hlm-skeleton class="h-8 w-48"></hlm-skeleton>
<hlm-skeleton class="h-6 w-64"></hlm-skeleton>
} @else {
<h1 class="text-2xl md:text-3xl font-bold leading-6">{{ userData()?.login }}</h1>
<div>
<a
class="md:text-lg font-medium text-muted-foreground mb-1 hover:text-github-accent-foreground"
href="https://github.com/{{ userData()?.login }}"
target="_blank"
rel="noopener noreferrer"
>
github.com/{{ userData()?.login }}
</a>
</div>
@if (displayFirstContribution()) {
<div class="flex items-center gap-2 text-muted-foreground font-medium text-sm md:text-base">
<ng-icon [svg]="octClockFill" size="16" />
Contributing since {{ displayFirstContribution() }}
</div>
}
<div class="flex items-center gap-2">
@for (repository of userData()?.repositories; track repository) {
<hlm-tooltip>
<div hlmBtn hlmTooltipTrigger class="size-10 bg-neutral-100 dark:bg-neutral-900/80 border border-input rounded-sm p-1" [aria-describedby]="repository">
<img [src]="getRepositoryImage(repository)" [alt]="repository" />
</div>
<span *brnTooltipContent>{{ repository }}</span>
</hlm-tooltip>
}
</div>
}
</div>
</div>
59 changes: 59 additions & 0 deletions webapp/src/app/user/header/header.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Component, computed, input } from '@angular/core';
import { NgIconComponent } from '@ng-icons/core';
import { octClockFill } from '@ng-icons/octicons';
import { HlmAvatarModule } from '@spartan-ng/ui-avatar-helm';
import { HlmSkeletonModule } from '@spartan-ng/ui-skeleton-helm';
import { HlmIconModule } from 'libs/ui/ui-icon-helm/src/index';
import { BrnTooltipContentDirective } from '@spartan-ng/ui-tooltip-brain';
import { HlmTooltipComponent, HlmTooltipTriggerDirective } from '@spartan-ng/ui-tooltip-helm';
import { HlmButtonModule } from '@spartan-ng/ui-button-helm';
import { LucideAngularModule } from 'lucide-angular';
import dayjs from 'dayjs';
import advancedFormat from 'dayjs/plugin/advancedFormat';

dayjs.extend(advancedFormat);

type UserHeaderProps = {
avatarUrl: string;
login: string;
firstContribution: string;
repositories: Set<string>;
};

const repoImages: { [key: string]: string } = {
Hephaestus: 'https://github.com/ls1intum/Hephaestus/raw/refs/heads/develop/docs/images/hammer.svg',
Artemis: 'https://artemis.in.tum.de/public/images/logo.png',
Athena: 'https://raw.githubusercontent.com/ls1intum/Athena/develop/playground/public/logo.png'
};

@Component({
selector: 'app-user-header',
standalone: true,
imports: [
LucideAngularModule,
NgIconComponent,
HlmAvatarModule,
HlmSkeletonModule,
HlmIconModule,
HlmTooltipComponent,
HlmTooltipTriggerDirective,
BrnTooltipContentDirective,
HlmButtonModule
],
templateUrl: './header.component.html'
})
export class UserHeaderComponent {
protected octClockFill = octClockFill;

isLoading = input(false);
userData = input<UserHeaderProps>();

displayFirstContribution = computed(() => {
if (this.userData()?.firstContribution) {
return dayjs(this.userData()?.firstContribution).format('Do [of] MMMM YYYY');
}
return null;
});

getRepositoryImage = (name: string) => (name ? repoImages[name.split('/')[1]] : null) || 'https://avatars.githubusercontent.com/u/11064260?v=4';
}
83 changes: 83 additions & 0 deletions webapp/src/app/user/header/header.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { argsToTemplate, Meta, StoryObj } from '@storybook/angular';
import dayjs from 'dayjs';
import { UserHeaderComponent } from './header.component';

type FlatArgs = {
isLoading: boolean;
avatarUrl: string;
login: string;
firstContribution: string;
repositories: string;
};

function flatArgsToProps(args: FlatArgs) {
return {
isLoading: args.isLoading,
userData: {
avatarUrl: args.avatarUrl,
login: args.login,
firstContribution: dayjs(args.firstContribution),
repositories: new Set(args.repositories.split(',').map((repo) => repo.trim()))
}
};
}

const meta: Meta<FlatArgs> = {
component: UserHeaderComponent,
tags: ['autodocs'],
args: {
isLoading: false,
avatarUrl: 'https://avatars.githubusercontent.com/u/11064260?v=4',
login: 'octocat',
firstContribution: dayjs().subtract(4, 'days').toISOString(),
repositories: 'ls1intum/Hephaestus, ls1intum/Artemis, ls1intum/Athena'
},
argTypes: {
isLoading: {
control: {
type: 'boolean'
}
},
firstContribution: {
control: {
type: 'date'
}
},
avatarUrl: {
control: {
type: 'text'
}
},
login: {
control: {
type: 'text'
}
},
repositories: {
control: {
type: 'text'
}
}
}
};

export default meta;

type Story = StoryObj<UserHeaderComponent>;

export const Default: Story = {
render: (args) => ({
props: flatArgsToProps(args as unknown as FlatArgs),
template: `<app-user-header ${argsToTemplate(flatArgsToProps(args as unknown as FlatArgs))} />`
})
};

export const IsLoading: Story = {
args: {
isLoading: true
},
render: (args) => ({
props: flatArgsToProps(args as unknown as FlatArgs),
template: `<app-user-header ${argsToTemplate(flatArgsToProps(args as unknown as FlatArgs))} />`
})
};
74 changes: 39 additions & 35 deletions webapp/src/app/user/issue-card/issue-card.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,53 @@
<div hlmCardContent variant="profile">
<div class="flex justify-between items-center text-sm text-github-muted-foreground">
<span class="font-medium flex justify-center items-center space-x-1">
@if (state() === 'OPEN') {
<ng-icon [svg]="octGitPullRequest" size="18" class="mr-1 text-github-success-foreground"></ng-icon>
@if (isLoading()) {
<hlm-skeleton class="h-4 w-16 lg:w-36"></hlm-skeleton>
} @else {
<ng-icon [svg]="octGitPullRequestClosed" size="18" class="mr-1 text-github-danger-foreground"></ng-icon>
}
@if (state() === 'OPEN') {
<ng-icon [svg]="octGitPullRequest" size="18" class="mr-1 text-github-success-foreground"></ng-icon>
} @else {
<ng-icon [svg]="octGitPullRequestClosed" size="18" class="mr-1 text-github-danger-foreground"></ng-icon>
}

{{ repositoryName() }} #{{ number() }} on {{ displayCreated().format('MMM D') }}
{{ repositoryName() }} #{{ number() }} on {{ displayCreated().format('MMM D') }}
}
</span>
<span class="flex items-center space-x-2">
<span class="text-github-success-foreground font-bold">+{{ additions() }}</span>
<span class="text-github-danger-foreground font-bold">-{{ deletions() }}</span>
@if (isLoading()) {
<hlm-skeleton class="size-4 bg-green-500/30"></hlm-skeleton>
<hlm-skeleton class="size-4 bg-destructive/20"></hlm-skeleton>
} @else {
<span class="text-github-success-foreground font-bold">+{{ additions() }}</span>
<span class="text-github-danger-foreground font-bold">-{{ deletions() }}</span>
}
</span>
</div>

<a class="flex justify-between font-medium mb-3 hover:text-github-accent-foreground" href="{{ url() }}" target="_blank" rel="noopener noreferrer">
<div [innerHTML]="displayTitle()" class="truncate"></div>
@if (reviews() && reviews()!.size > 0 && getMostRecentReview(); as review) {
@if (review.state === 'APPROVED') {
<ng-icon [svg]="octCheck" size="16" class="text-github-success-foreground"></ng-icon>
} @else if (review.state === 'DISMISSED') {
<ng-icon [svg]="octX" size="16" class="text-github-danger-foreground"></ng-icon>
} @else if (review.state === 'COMMENTED') {
<ng-icon [svg]="octComment" size="16" class="text-github-muted-foreground"></ng-icon>
} @else {
<ng-icon [svg]="octFileDiff" size="16" class="text-github-danger-foreground"></ng-icon>
}
<a class="containerSize flex justify-between font-medium mb-3 hover:text-github-accent-foreground" href="{{ url() }}" target="_blank" rel="noopener noreferrer">
@if (isLoading()) {
<hlm-skeleton class="h-6 w-3/4"></hlm-skeleton>
} @else {
<div [innerHTML]="displayTitle()" class="truncate"></div>
}
</a>
</div>
<div hlmCardFooter class="flex gap-2 flex-wrap gap-y-2 p-0">
@for (label of pullRequestLabels(); track label.name) {
@let labelColors = hexToRgb(label.color ?? 'FFFFFF');
<span
class="px-2 py-0.5 rounded-[2rem] text-xs font-medium dark:border gh-label"
[style.--label-r]="labelColors.r"
[style.--label-g]="labelColors.g"
[style.--label-b]="labelColors.b"
[style.--label-h]="labelColors.h"
[style.--label-s]="labelColors.s"
[style.--label-l]="labelColors.l"
>
{{ label.name }}
</span>
}
</div>
@if (!isLoading()) {
<div hlmCardFooter class="flex flex-wrap gap-2 p-0 space-x-0">
@for (label of pullRequestLabels(); track label.name) {
@let labelColors = hexToRgb(label.color ?? 'FFFFFF');
<span
class="px-2 py-0.5 rounded-[2rem] text-xs font-medium dark:border gh-label"
[style.--label-r]="labelColors.r"
[style.--label-g]="labelColors.g"
[style.--label-b]="labelColors.b"
[style.--label-h]="labelColors.h"
[style.--label-s]="labelColors.s"
[style.--label-l]="labelColors.l"
>
{{ label.name }}
</span>
}
</div>
}
</div>
31 changes: 11 additions & 20 deletions webapp/src/app/user/issue-card/issue-card.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, computed, input } from '@angular/core';
import { PullRequest, PullRequestLabel, PullRequestReviewDTO } from '@app/core/modules/openapi';
import { PullRequest, PullRequestLabel } from '@app/core/modules/openapi';
import { NgIcon } from '@ng-icons/core';
import { octCheck, octComment, octFileDiff, octGitPullRequest, octGitPullRequestClosed, octX } from '@ng-icons/octicons';
import { HlmCardModule } from '@spartan-ng/ui-card-helm';
Expand All @@ -15,17 +15,17 @@ import { cn } from '@app/utils';
standalone: true
})
export class IssueCardComponent {
isLoading = input(false);
class = input('');
title = input.required<string>();
number = input.required<number>();
additions = input.required<number>();
deletions = input.required<number>();
url = input.required<string>();
repositoryName = input.required<string>();
reviews = input<Set<PullRequestReviewDTO>>();
createdAt = input.required<string>();
state = input.required<PullRequest.StateEnum>();
pullRequestLabels = input.required<Set<PullRequestLabel> | undefined>();
title = input<string>();
number = input<number>();
additions = input<number>();
deletions = input<number>();
url = input<string>();
repositoryName = input<string>();
createdAt = input<string>();
state = input<PullRequest.StateEnum>();
pullRequestLabels = input<Set<PullRequestLabel>>();
protected readonly octCheck = octCheck;
protected readonly octX = octX;
protected readonly octComment = octComment;
Expand All @@ -37,15 +37,6 @@ export class IssueCardComponent {
displayTitle = computed(() => (this.title() ?? '').replace(/`([^`]+)`/g, '<code class="textCode">$1</code>'));
computedClass = computed(() => cn('w-72', this.class()));

getMostRecentReview() {
if (!this.reviews()) {
return null;
}
return Array.from(this.reviews()!).reduce((latest, review) => {
return new Date(review.updatedAt || 0) > new Date(latest.updatedAt || 0) ? review : latest;
});
}

hexToRgb(hex: string) {
const bigint = parseInt(hex, 16);
const r = (bigint >> 16) & 255;
Expand Down
17 changes: 0 additions & 17 deletions webapp/src/app/user/issue-card/issue-card.stories.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Meta, StoryObj } from '@storybook/angular';
import { IssueCardComponent } from './issue-card.component';
import { PullRequestReviewDTO } from '@app/core/modules/openapi';

const meta: Meta<IssueCardComponent> = {
component: IssueCardComponent,
Expand All @@ -24,22 +23,6 @@ export const Default: Story = {
pullRequestLabels: new Set([
{ name: 'bug', color: 'f00000' },
{ name: 'enhancement', color: '008000' }
]),
reviews: new Set<PullRequestReviewDTO>([
{
id: 0,
createdAt: 'Jan 2',
updatedAt: 'Jan 2',
submittedAt: 'Jan 2',
state: 'APPROVED'
},
{
id: 1,
createdAt: 'Jan 4',
updatedAt: 'Jan 4',
submittedAt: 'Jan 4',
state: 'CHANGES_REQUESTED'
}
])
}
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
@if (isLoading()) {
<div hlmCard variant="profile">
<div hlmCardContent>
<hlm-skeleton class="h-4 w-56 sm:w-72 md:w-80"></hlm-skeleton>
<div hlmCardContent variant="profile">
<hlm-skeleton class="h-4 w-1/2"></hlm-skeleton>
<div class="flex items-center gap-2 font-medium">
<hlm-skeleton [class]="'h-7 w-7 ' + this.skeletonColorForReviewState()"></hlm-skeleton>
<hlm-skeleton class="h-6 w-64 sm:w-80 md:w-96"></hlm-skeleton>
<hlm-skeleton class="h-6 w-3/4"></hlm-skeleton>
</div>
</div>
</div>
Expand Down
Loading

0 comments on commit d13510b

Please sign in to comment.