diff --git a/webapp/package-lock.json b/webapp/package-lock.json
index 42280e4f..3a1e52f2 100644
--- a/webapp/package-lock.json
+++ b/webapp/package-lock.json
@@ -24,6 +24,7 @@
"autoprefixer": "10.4.20",
"class-variance-authority": "0.7.0",
"clsx": "2.1.1",
+ "dayjs": "^1.11.13",
"lucide-angular": "0.429.0",
"postcss": "8.4.41",
"rxjs": "7.8.1",
@@ -9136,6 +9137,12 @@
"node": ">=4.0"
}
},
+ "node_modules/dayjs": {
+ "version": "1.11.13",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
+ "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
+ "license": "MIT"
+ },
"node_modules/debug": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
diff --git a/webapp/package.json b/webapp/package.json
index cb31cc2e..db4e66e3 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -36,6 +36,7 @@
"autoprefixer": "10.4.20",
"class-variance-authority": "0.7.0",
"clsx": "2.1.1",
+ "dayjs": "^1.11.13",
"lucide-angular": "0.429.0",
"postcss": "8.4.41",
"rxjs": "7.8.1",
diff --git a/webapp/src/app/core/IssueCard/issue-card.component.html b/webapp/src/app/core/IssueCard/issue-card.component.html
new file mode 100644
index 00000000..a966e74d
--- /dev/null
+++ b/webapp/src/app/core/IssueCard/issue-card.component.html
@@ -0,0 +1,37 @@
+
+
+
+ @if (state() === 'OPEN') {
+
+ } @else {
+
+ }
+
+ {{ repositoryName() }} #{{ number() }} on {{ createdAt().format('MMM D') }}
+
+
+ +{{ additions() }}
+ -{{ deletions() }}
+
+
+
+
+ {{ title() }}
+ @if (getMostRecentReview(); as review) {
+ @if (review.state === 'APPROVED') {
+
+ } @else if (review.state === 'DISMISSED') {
+
+ } @else if (review.state === 'COMMENTED') {
+
+ } @else {
+
+ }
+ }
+
+
+ @for (label of pullRequestLabels(); track label.name) {
+ {{ label.name }}
+ }
+
+
diff --git a/webapp/src/app/core/IssueCard/issue-card.component.ts b/webapp/src/app/core/IssueCard/issue-card.component.ts
new file mode 100644
index 00000000..e4fedf96
--- /dev/null
+++ b/webapp/src/app/core/IssueCard/issue-card.component.ts
@@ -0,0 +1,41 @@
+import { Component, input } from '@angular/core';
+import { PullRequestLabel, PullRequestReview } from '@app/core/modules/openapi';
+import { NgIcon } from '@ng-icons/core';
+import { octCheck, octComment, octFileDiff, octGitPullRequest, octGitPullRequestClosed, octX } from '@ng-icons/octicons';
+import { Dayjs } from 'dayjs';
+import { NgStyle } from '@angular/common';
+
+@Component({
+ selector: 'app-issue-card',
+ templateUrl: './issue-card.component.html',
+ imports: [NgIcon, NgStyle],
+ standalone: true
+})
+export class IssueCardComponent {
+ title = input.required();
+ number = input.required();
+ additions = input.required();
+ deletions = input.required();
+ url = input.required();
+ repositoryName = input.required();
+ reviews = input.required>();
+ createdAt = input.required();
+ state = input.required();
+ pullRequestLabels = input.required>();
+ protected readonly octCheck = octCheck;
+ protected readonly octX = octX;
+ protected readonly octComment = octComment;
+ protected readonly octGitPullRequest = octGitPullRequest;
+ protected readonly octFileDiff = octFileDiff;
+ protected readonly octGitPullRequestClosed = octGitPullRequestClosed;
+
+ getMostRecentReview() {
+ return Array.from(this.reviews() || []).reduce((latest: PullRequestReview, review: PullRequestReview) => {
+ return new Date(review.updatedAt || 0) > new Date(latest.updatedAt || 0) ? review : latest;
+ });
+ }
+
+ openIssue() {
+ window.open(this.url());
+ }
+}
diff --git a/webapp/src/app/core/IssueCard/issue-card.stories.ts b/webapp/src/app/core/IssueCard/issue-card.stories.ts
new file mode 100644
index 00000000..54b2d620
--- /dev/null
+++ b/webapp/src/app/core/IssueCard/issue-card.stories.ts
@@ -0,0 +1,40 @@
+import { Meta, StoryObj } from '@storybook/angular';
+import { IssueCardComponent } from './issue-card.component';
+import dayjs from 'dayjs';
+
+const meta: Meta = {
+ title: 'Component/IssueCard',
+ component: IssueCardComponent,
+ tags: ['autodocs'] // Auto-generate docs if enabled
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ title: 'Add feature X',
+ number: 12,
+ additions: 10,
+ deletions: 5,
+ url: 'http://example.com',
+ state: 'OPEN',
+ repositoryName: 'Artemis',
+ createdAt: dayjs('Jan 1'),
+ pullRequestLabels: [
+ { name: 'bug', color: 'red' },
+ { name: 'enhancement', color: 'green' }
+ ],
+ reviews: [
+ {
+ state: 'APPROVED',
+ updatedAt: 'Jan 2'
+ },
+ {
+ state: 'CHANGES_REQUESTED',
+ updatedAt: 'Jan 4'
+ }
+ ]
+ }
+};
diff --git a/webapp/src/app/ui/app-issue-card/app-issue-card.component.html b/webapp/src/app/ui/app-issue-card/app-issue-card.component.html
deleted file mode 100644
index 931bbaa2..00000000
--- a/webapp/src/app/ui/app-issue-card/app-issue-card.component.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
- {{ pullRequest().repository?.name }} #{{ pullRequest().number }} on {{ pullRequest().createdAt }}
-
-
- +{{ pullRequest().additions }}
- -{{ pullRequest().deletions }}
-
-
-
-
- {{ pullRequest().title }}
- @if (getMostRecentReview(); as review) {
- @if (review.state === 'APPROVED') {
-
- } @else if (review.state === 'DISMISSED') {
-
- } @else if (review.state === 'COMMENTED') {
-
- } @else {
-
- }
- }
-
-
- @for (label of pullRequest().pullRequestLabels; track label.name) {
- {{ label.name }}
- }
-
-
diff --git a/webapp/src/app/ui/app-issue-card/app-issue-card.component.ts b/webapp/src/app/ui/app-issue-card/app-issue-card.component.ts
deleted file mode 100644
index 75c93950..00000000
--- a/webapp/src/app/ui/app-issue-card/app-issue-card.component.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Component, input } from '@angular/core';
-import { PullRequest, PullRequestReview } from '@app/core/modules/openapi';
-import { NgIcon } from '@ng-icons/core';
-import { octCheck, octComment, octFileDiff, octGitPullRequest, octX } from '@ng-icons/octicons';
-
-@Component({
- selector: 'app-issue-card',
- templateUrl: './app-issue-card.component.html',
- imports: [NgIcon],
- standalone: true
-})
-export class AppIssueCardComponent {
- pullRequest = input.required();
- protected readonly octCheck = octCheck;
- protected readonly octX = octX;
- protected readonly octComment = octComment;
- protected readonly octGitPullRequest = octGitPullRequest;
-
- getMostRecentReview() {
- if (!this.pullRequest() || !this.pullRequest().reviews) {
- return null;
- }
-
- return Array.from(this.pullRequest().reviews || []).reduce((latest: PullRequestReview, review: PullRequestReview) => {
- return new Date(review.updatedAt || 0) > new Date(latest.updatedAt || 0) ? review : latest;
- });
- }
-
- protected readonly octFileDiff = octFileDiff;
-}
diff --git a/webapp/src/app/ui/app-issue-card/app-issue-card.stories.ts b/webapp/src/app/ui/app-issue-card/app-issue-card.stories.ts
deleted file mode 100644
index 8e5f0eac..00000000
--- a/webapp/src/app/ui/app-issue-card/app-issue-card.stories.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Meta, StoryObj } from '@storybook/angular';
-import { AppIssueCardComponent } from './app-issue-card.component';
-import { PullRequest, PullRequestReview, Repository } from '@app/core/modules/openapi';
-
-const meta: Meta = {
- title: 'UI/AppIssueCard',
- component: AppIssueCardComponent,
- tags: ['autodocs'] // Auto-generate docs if enabled
-};
-
-export default meta;
-
-type Story = StoryObj;
-
-const repo: Repository = {
- name: 'Artemis',
- nameWithOwner: 'artemis-education/artemis',
- defaultBranch: 'master',
- visibility: 'PUBLIC',
- url: 'http://example.com'
-};
-
-const reviews = new Set([
- {
- state: 'APPROVED',
- updatedAt: 'Jan 2'
- },
- {
- state: 'CHANGES_REQUESTED',
- updatedAt: 'Jan 4'
- }
-]);
-
-const pullRequest: PullRequest = {
- title: 'Add feature X',
- number: 12,
- additions: 10,
- deletions: 5,
- url: 'http://example.com',
- state: 'OPEN',
- repository: repo,
- createdAt: 'Jan 1',
- pullRequestLabels: new Set([
- { name: 'bug', color: 'red' },
- { name: 'enhancement', color: 'green' }
- ]),
- reviews: reviews
-};
-
-export const Default: Story = {
- args: { pullRequest }
-};