diff --git a/webapp/.storybook/preview.ts b/webapp/.storybook/preview.ts index f7573730..93bed5ea 100644 --- a/webapp/.storybook/preview.ts +++ b/webapp/.storybook/preview.ts @@ -55,7 +55,7 @@ const preview: Preview = { light: '', dark: 'dark bg-background', }, - defaultTheme: 'dark', + defaultTheme: 'light', }), applicationConfig(appConfig), ], diff --git a/webapp/src/app/@types/github.d.ts b/webapp/src/app/@types/github.d.ts new file mode 100644 index 00000000..60fa31f8 --- /dev/null +++ b/webapp/src/app/@types/github.d.ts @@ -0,0 +1,10 @@ +export namespace GitHub { + export interface Contributor { + id: number; + login: string; + url: string; + html_url: string; + avatar_url: string; + // More fields can be added here + } +} diff --git a/webapp/src/app/about/about.component.html b/webapp/src/app/about/about.component.html new file mode 100644 index 00000000..f3fc9ecf --- /dev/null +++ b/webapp/src/app/about/about.component.html @@ -0,0 +1,53 @@ +
+

About

+

+ Hephaestus leverages generative AI to streamline software development and enhance developer training. Focused on improving every phase of the software development lifecycle + (SDLC) and supporting agile practices, Hephaestus helps teams work more efficiently and adhere to best practices. +

+

Team

+ @if (query.isPending()) { + Loading... + } @else if (query.error()) { + An error has occurred + } + + @if (projectManager(); as projectManager) { +
+ + + + {{ projectManager.login.slice(0, 1).toUpperCase() }} + + +
+
Felix T.J. Dietrich
+
Project Manager
+ Website +
+
+ } + + @if (contributors(); as contributors) { +

Contributors

+
+ @for (contributor of contributors; track contributor.id) { + + + + {{ contributor.login.slice(0, 1).toUpperCase() }} + + + } +
+ } +
diff --git a/webapp/src/app/about/about.component.ts b/webapp/src/app/about/about.component.ts new file mode 100644 index 00000000..d28bd965 --- /dev/null +++ b/webapp/src/app/about/about.component.ts @@ -0,0 +1,42 @@ +import { HttpClient } from '@angular/common/http'; +import { Component, computed, inject } from '@angular/core'; +import { injectQuery } from '@tanstack/angular-query-experimental'; +import { lastValueFrom } from 'rxjs'; +import { AvatarComponent } from 'app/ui/avatar/avatar.component'; +import { AvatarImageComponent } from 'app/ui/avatar/avatar-image.component'; +import { AvatarFallbackComponent } from 'app/ui/avatar/avatar-fallback.component'; +import { ButtonComponent } from 'app/ui/button/button.component'; +import { GitHub } from 'app/@types/github'; + +@Component({ + selector: 'app-about', + standalone: true, + imports: [AvatarComponent, AvatarImageComponent, AvatarFallbackComponent, ButtonComponent], + templateUrl: './about.component.html' +}) +export class AboutComponent { + http = inject(HttpClient); + + query = injectQuery(() => ({ + queryKey: ['contributors'], + queryFn: async () => lastValueFrom(this.http.get('https://api.github.com/repos/ls1intum/hephaestus/contributors')) as Promise, + gcTime: Infinity + })); + + projectManager = computed(() => { + const data = this.query.data(); + if (!data) { + return undefined; + } + // 5898705 is the id of the project manager Felix T.J. Dietrich + return data.find((contributor) => contributor.id === 5898705); + }); + + contributors = computed(() => { + const data = this.query.data(); + if (!data) { + return undefined; + } + return data.filter((contributor) => contributor.id !== 5898705); + }); +} diff --git a/webapp/src/app/about/about.stories.ts b/webapp/src/app/about/about.stories.ts new file mode 100644 index 00000000..c02118cd --- /dev/null +++ b/webapp/src/app/about/about.stories.ts @@ -0,0 +1,18 @@ +import { type Meta, type StoryObj } from '@storybook/angular'; +import { AboutComponent } from './about.component'; + +const meta: Meta = { + title: 'Pages/About', + component: AboutComponent, + tags: ['autodocs'] +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: `` + }) +}; diff --git a/webapp/src/app/app.component.html b/webapp/src/app/app.component.html index 9b87933d..1d980eee 100644 --- a/webapp/src/app/app.component.html +++ b/webapp/src/app/app.component.html @@ -1,15 +1,26 @@ -
-
- +
+
+ +
+ + diff --git a/webapp/src/app/app.component.ts b/webapp/src/app/app.component.ts index b75100bd..b30ba0c0 100644 --- a/webapp/src/app/app.component.ts +++ b/webapp/src/app/app.component.ts @@ -1,14 +1,12 @@ import { Component } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; -import { CounterComponent } from './example/counter/counter.component'; -import { HelloComponent } from './example/hello/hello.component'; +import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { ThemeSwitcherComponent } from './components/theme-switcher/theme-switcher.component'; import { LucideAngularModule } from 'lucide-angular'; @Component({ selector: 'app-root', standalone: true, - imports: [RouterOutlet, CounterComponent, HelloComponent, LucideAngularModule, ThemeSwitcherComponent], + imports: [RouterOutlet, LucideAngularModule, ThemeSwitcherComponent, RouterLink, RouterLinkActive], templateUrl: './app.component.html', styles: [] }) diff --git a/webapp/src/app/app.routes.ts b/webapp/src/app/app.routes.ts index dc39edb5..9060460d 100644 --- a/webapp/src/app/app.routes.ts +++ b/webapp/src/app/app.routes.ts @@ -1,3 +1,8 @@ import { Routes } from '@angular/router'; +import { AboutComponent } from 'app/about/about.component'; +import { MainComponent } from 'app/main/main.component'; -export const routes: Routes = []; +export const routes: Routes = [ + { path: '', component: MainComponent }, + { path: 'about', component: AboutComponent } +]; diff --git a/webapp/src/app/main/main.component.html b/webapp/src/app/main/main.component.html new file mode 100644 index 00000000..bbeb09d3 --- /dev/null +++ b/webapp/src/app/main/main.component.html @@ -0,0 +1,5 @@ +
+ + +
+ diff --git a/webapp/src/app/main/main.component.ts b/webapp/src/app/main/main.component.ts new file mode 100644 index 00000000..670ad21f --- /dev/null +++ b/webapp/src/app/main/main.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { CounterComponent } from 'app/example/counter/counter.component'; +import { HelloComponent } from 'app/example/hello/hello.component'; + +@Component({ + selector: 'app-main', + standalone: true, + imports: [CounterComponent, HelloComponent], + templateUrl: './main.component.html' +}) +export class MainComponent {} diff --git a/webapp/src/app/ui/avatar/avatar-fallback.component.ts b/webapp/src/app/ui/avatar/avatar-fallback.component.ts index 3948454c..1d4ad69f 100644 --- a/webapp/src/app/ui/avatar/avatar-fallback.component.ts +++ b/webapp/src/app/ui/avatar/avatar-fallback.component.ts @@ -3,6 +3,7 @@ import { cn } from 'app/utils'; import { ClassValue } from 'clsx'; import { injectAvatarConfig } from './avatar-config'; import { injectAvatar } from './avatar.component'; +import { Subject, takeUntil, timer } from 'rxjs'; @Component({ selector: 'app-avatar-fallback', @@ -17,7 +18,7 @@ export class AvatarFallbackComponent implements OnInit, OnDestroy { private readonly avatar = injectAvatar(); private config = injectAvatarConfig(); private delayElapsed = signal(false); - private timeout: NodeJS.Timeout | null = null; + private destroy$ = new Subject(); class = input(); delayMs = input(this.config.delayMs); @@ -25,12 +26,13 @@ export class AvatarFallbackComponent implements OnInit, OnDestroy { visible = computed(() => this.avatar.state() !== 'loaded' && this.delayElapsed()); ngOnInit(): void { - this.timeout = setTimeout(() => this.delayElapsed.set(true), this.delayMs()); + timer(this.delayMs()) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => this.delayElapsed.set(true)); } ngOnDestroy(): void { - if (this.timeout) { - clearTimeout(this.timeout); - } + this.destroy$.next(); + this.destroy$.complete(); } } diff --git a/webapp/src/app/ui/avatar/avatar.stories.ts b/webapp/src/app/ui/avatar/avatar.stories.ts index d77b60e4..77507492 100644 --- a/webapp/src/app/ui/avatar/avatar.stories.ts +++ b/webapp/src/app/ui/avatar/avatar.stories.ts @@ -46,16 +46,13 @@ export default meta; type Story = StoryObj; export const Default: Story = { - render: (args) => { - console.log(args); - return { - props: args, - template: ` + render: (args) => ({ + props: args, + template: ` CN ` - }; - } + }) };