From 6aed655af3a84aad1993ecf8d863c175dd969d9f Mon Sep 17 00:00:00 2001
From: SandilBandara
Date: Sun, 9 Aug 2026 20:11:03 +1000
Subject: [PATCH 1/3] Add reusable project card component
---
.../project-card/project-card.component.html | 28 ++++++++
.../project-card/project-card.component.scss | 65 +++++++++++++++++++
.../project-card.component.spec.ts | 64 ++++++++++++++++++
.../project-card/project-card.component.ts | 23 +++++++
src/app/doubtfire-angular.module.ts | 4 ++
5 files changed, 184 insertions(+)
create mode 100644 src/app/common/project-card/project-card.component.html
create mode 100644 src/app/common/project-card/project-card.component.scss
create mode 100644 src/app/common/project-card/project-card.component.spec.ts
create mode 100644 src/app/common/project-card/project-card.component.ts
diff --git a/src/app/common/project-card/project-card.component.html b/src/app/common/project-card/project-card.component.html
new file mode 100644
index 0000000000..053f807bc0
--- /dev/null
+++ b/src/app/common/project-card/project-card.component.html
@@ -0,0 +1,28 @@
+
+
+
+
+ {{ project.progressSummary }}
+
+
+
+ {{ project.description }}
+
+
+
+ Open project
+
+
\ No newline at end of file
diff --git a/src/app/common/project-card/project-card.component.scss b/src/app/common/project-card/project-card.component.scss
new file mode 100644
index 0000000000..d32a89c167
--- /dev/null
+++ b/src/app/common/project-card/project-card.component.scss
@@ -0,0 +1,65 @@
+.project-card {
+ border: 1px solid #d6d6d6;
+ border-radius: 12px;
+ padding: 1rem;
+ background: #ffffff;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ max-width: 420px;
+}
+
+.project-card__header {
+ display: flex;
+ justify-content: space-between;
+ gap: 1rem;
+ align-items: flex-start;
+}
+
+.project-card__unit {
+ margin: 0 0 0.25rem;
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: #555555;
+}
+
+.project-card__title {
+ margin: 0;
+ font-size: 1.1rem;
+ line-height: 1.3;
+}
+
+.project-card__status {
+ border-radius: 999px;
+ padding: 0.25rem 0.6rem;
+ font-size: 0.8rem;
+ font-weight: 600;
+ background: #eeeeee;
+ white-space: nowrap;
+}
+
+.project-card__summary,
+.project-card__description {
+ margin: 0;
+ line-height: 1.5;
+}
+
+.project-card__link {
+ align-self: flex-start;
+ font-weight: 600;
+ text-decoration: underline;
+}
+
+@media (max-width: 520px) {
+ .project-card {
+ max-width: 100%;
+ }
+
+ .project-card__header {
+ flex-direction: column;
+ }
+
+ .project-card__status {
+ align-self: flex-start;
+ }
+}
\ No newline at end of file
diff --git a/src/app/common/project-card/project-card.component.spec.ts b/src/app/common/project-card/project-card.component.spec.ts
new file mode 100644
index 0000000000..4c5d9d7043
--- /dev/null
+++ b/src/app/common/project-card/project-card.component.spec.ts
@@ -0,0 +1,64 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ProjectCardComponent, ProjectCardData } from './project-card.component';
+
+describe('ProjectCardComponent', () => {
+ let component: ProjectCardComponent;
+ let fixture: ComponentFixture;
+
+ const sampleProject: ProjectCardData = {
+ title: 'Cross-Project Dashboard',
+ unitCode: 'CPD-F02',
+ status: 'In progress',
+ progressSummary: 'Reusable dashboard project-card component is being prepared.',
+ description: 'This card summarises one project without hardcoding the dashboard data.',
+ destinationUrl: '/projects/cpd-f02'
+ };
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ProjectCardComponent]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(ProjectCardComponent);
+ component = fixture.componentInstance;
+ component.project = sampleProject;
+ fixture.detectChanges();
+ });
+
+ it('should create the project card component', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should display the supplied project details', () => {
+ const text = fixture.nativeElement.textContent;
+
+ expect(text).toContain('Cross-Project Dashboard');
+ expect(text).toContain('CPD-F02');
+ expect(text).toContain('In progress');
+ expect(text).toContain('Reusable dashboard project-card component is being prepared.');
+ });
+
+ it('should provide an accessible project link', () => {
+ const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a');
+
+ expect(link).toBeTruthy();
+ expect(link.getAttribute('href')).toBe('/projects/cpd-f02');
+ expect(link.getAttribute('aria-label')).toBe('Open project details');
+ });
+
+ it('should handle missing optional fields', () => {
+ component.project = {
+ title: 'Sample Project',
+ status: 'Completed',
+ progressSummary: 'Core details are still displayed.',
+ destinationUrl: '/projects/sample'
+ };
+
+ fixture.detectChanges();
+
+ const text = fixture.nativeElement.textContent;
+ expect(text).toContain('Sample Project');
+ expect(text).toContain('Completed');
+ expect(text).toContain('Core details are still displayed.');
+ });
+});
\ No newline at end of file
diff --git a/src/app/common/project-card/project-card.component.ts b/src/app/common/project-card/project-card.component.ts
new file mode 100644
index 0000000000..521a42969e
--- /dev/null
+++ b/src/app/common/project-card/project-card.component.ts
@@ -0,0 +1,23 @@
+import { Component, Input } from '@angular/core';
+
+export interface ProjectCardData {
+ title: string;
+ status: string;
+ progressSummary: string;
+ destinationUrl: string;
+ unitCode?: string;
+ description?: string;
+}
+
+@Component({
+ selector: 'project-card',
+ templateUrl: './project-card.component.html',
+ styleUrls: ['./project-card.component.scss']
+})
+export class ProjectCardComponent {
+ @Input() project!: ProjectCardData;
+
+ get hasOptionalDetails(): boolean {
+ return !!this.project?.unitCode || !!this.project?.description;
+ }
+}
\ No newline at end of file
diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts
index c4abd459bc..ffc3c9688b 100644
--- a/src/app/doubtfire-angular.module.ts
+++ b/src/app/doubtfire-angular.module.ts
@@ -1,4 +1,7 @@
// Lottie animation module
+}
+import {ProjectCardComponent} from './common/project-card/project-card.component';
+// Lottie animation module
// import {LottieModule, LottieCacheModule} from 'ngx-lottie';
import {PickerModule} from '@ctrl/ngx-emoji-mart';
import {EmojiModule} from '@ctrl/ngx-emoji-mart/ngx-emoji';
@@ -566,6 +569,7 @@ const DEFAULT_TOOLTIP_OPTIONS: MatTooltipDefaultOptions = {
CreateNewUnitModalContentComponent,
TiiActionLogComponent,
FChipComponent,
+ ProjectCardComponent,
UnitCodeComponent,
NewTeachingPeriodDialogComponent,
FileViewerComponent,
From 251adb8c19f731fb0254d6436e37605a0de0a09a Mon Sep 17 00:00:00 2001
From: SandilBandara
Date: Wed, 26 Aug 2026 00:50:34 +1000
Subject: [PATCH 2/3] fix(project-card): update Angular control flow
---
.../project-card/project-card.component.html | 50 ++++++++++---------
.../project-card.component.spec.ts | 17 ++++---
.../project-card/project-card.component.ts | 7 +--
src/app/doubtfire-angular.module.ts | 4 +-
4 files changed, 40 insertions(+), 38 deletions(-)
diff --git a/src/app/common/project-card/project-card.component.html b/src/app/common/project-card/project-card.component.html
index 053f807bc0..6c9046a0f0 100644
--- a/src/app/common/project-card/project-card.component.html
+++ b/src/app/common/project-card/project-card.component.html
@@ -1,28 +1,30 @@
-
-
-
- {{ project.progressSummary }}
-
+
+ {{ project.progressSummary }}
+
-
- {{ project.description }}
-
+ @if (project.description) {
+
+ {{ project.description }}
+
+ }
-
- Open project
-
-
\ No newline at end of file
+
+ Open project
+
+
+}
diff --git a/src/app/common/project-card/project-card.component.spec.ts b/src/app/common/project-card/project-card.component.spec.ts
index 4c5d9d7043..5342acfc3d 100644
--- a/src/app/common/project-card/project-card.component.spec.ts
+++ b/src/app/common/project-card/project-card.component.spec.ts
@@ -1,5 +1,5 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { ProjectCardComponent, ProjectCardData } from './project-card.component';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {ProjectCardComponent, ProjectCardData} from './project-card.component';
describe('ProjectCardComponent', () => {
let component: ProjectCardComponent;
@@ -11,12 +11,12 @@ describe('ProjectCardComponent', () => {
status: 'In progress',
progressSummary: 'Reusable dashboard project-card component is being prepared.',
description: 'This card summarises one project without hardcoding the dashboard data.',
- destinationUrl: '/projects/cpd-f02'
+ destinationUrl: '/projects/cpd-f02',
};
beforeEach(async () => {
await TestBed.configureTestingModule({
- declarations: [ProjectCardComponent]
+ declarations: [ProjectCardComponent],
}).compileComponents();
fixture = TestBed.createComponent(ProjectCardComponent);
@@ -47,18 +47,19 @@ describe('ProjectCardComponent', () => {
});
it('should handle missing optional fields', () => {
- component.project = {
+ fixture.componentRef.setInput('project', {
title: 'Sample Project',
status: 'Completed',
progressSummary: 'Core details are still displayed.',
- destinationUrl: '/projects/sample'
- };
+ destinationUrl: '/projects/sample',
+ });
fixture.detectChanges();
const text = fixture.nativeElement.textContent;
+
expect(text).toContain('Sample Project');
expect(text).toContain('Completed');
expect(text).toContain('Core details are still displayed.');
});
-});
\ No newline at end of file
+});
diff --git a/src/app/common/project-card/project-card.component.ts b/src/app/common/project-card/project-card.component.ts
index 521a42969e..fa86b34983 100644
--- a/src/app/common/project-card/project-card.component.ts
+++ b/src/app/common/project-card/project-card.component.ts
@@ -1,4 +1,4 @@
-import { Component, Input } from '@angular/core';
+import {Component, Input} from '@angular/core';
export interface ProjectCardData {
title: string;
@@ -11,8 +11,9 @@ export interface ProjectCardData {
@Component({
selector: 'project-card',
+ standalone: false,
templateUrl: './project-card.component.html',
- styleUrls: ['./project-card.component.scss']
+ styleUrls: ['./project-card.component.scss'],
})
export class ProjectCardComponent {
@Input() project!: ProjectCardData;
@@ -20,4 +21,4 @@ export class ProjectCardComponent {
get hasOptionalDetails(): boolean {
return !!this.project?.unitCode || !!this.project?.description;
}
-}
\ No newline at end of file
+}
diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts
index ffc3c9688b..1502bc99b3 100644
--- a/src/app/doubtfire-angular.module.ts
+++ b/src/app/doubtfire-angular.module.ts
@@ -1,7 +1,4 @@
// Lottie animation module
-}
-import {ProjectCardComponent} from './common/project-card/project-card.component';
-// Lottie animation module
// import {LottieModule, LottieCacheModule} from 'ngx-lottie';
import {PickerModule} from '@ctrl/ngx-emoji-mart';
import {EmojiModule} from '@ctrl/ngx-emoji-mart/ngx-emoji';
@@ -221,6 +218,7 @@ import {IsActiveUnitRole} from './common/pipes/is-active-unit-role.pipe';
import {LocalizedDatePipe} from './common/pipes/localized-date.pipe';
import {MarkedPipe} from './common/pipes/marked.pipe';
import {SafePipe} from './common/pipes/safe.pipe';
+import {ProjectCardComponent} from './common/project-card/project-card.component';
import {ProjectProgressBarComponent} from './common/project-progress-bar/project-progress-bar.component';
import {ProjectProgressGaugeComponent} from './common/project-progress/project-progress-gauge.component';
import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component';
From e3b9671979e68488cc20c424afc0b8a1689aa53c Mon Sep 17 00:00:00 2001
From: maplefoxgit
Date: Thu, 27 Aug 2026 13:41:56 +1000
Subject: [PATCH 3/3] fix(project-card): improve accessible labels
---
.../project-card/project-card.component.html | 10 +++++++---
.../project-card/project-card.component.scss | 14 +++++++++++++-
.../project-card/project-card.component.spec.ts | 11 ++++++++++-
.../common/project-card/project-card.component.ts | 4 ----
4 files changed, 30 insertions(+), 9 deletions(-)
diff --git a/src/app/common/project-card/project-card.component.html b/src/app/common/project-card/project-card.component.html
index 6c9046a0f0..5300e34f54 100644
--- a/src/app/common/project-card/project-card.component.html
+++ b/src/app/common/project-card/project-card.component.html
@@ -8,8 +8,8 @@
{{ project.title }}
-
- {{ project.status }}
+
+ Project status: {{ project.status }}
@@ -23,7 +23,11 @@ {{ project.title }}
}
-
+
Open project
diff --git a/src/app/common/project-card/project-card.component.scss b/src/app/common/project-card/project-card.component.scss
index d32a89c167..e90278716d 100644
--- a/src/app/common/project-card/project-card.component.scss
+++ b/src/app/common/project-card/project-card.component.scss
@@ -38,6 +38,18 @@
white-space: nowrap;
}
+.project-card__status-label {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
.project-card__summary,
.project-card__description {
margin: 0;
@@ -62,4 +74,4 @@
.project-card__status {
align-self: flex-start;
}
-}
\ No newline at end of file
+}
diff --git a/src/app/common/project-card/project-card.component.spec.ts b/src/app/common/project-card/project-card.component.spec.ts
index 5342acfc3d..5d8a798c81 100644
--- a/src/app/common/project-card/project-card.component.spec.ts
+++ b/src/app/common/project-card/project-card.component.spec.ts
@@ -43,7 +43,13 @@ describe('ProjectCardComponent', () => {
expect(link).toBeTruthy();
expect(link.getAttribute('href')).toBe('/projects/cpd-f02');
- expect(link.getAttribute('aria-label')).toBe('Open project details');
+ expect(link.getAttribute('aria-label')).toBe('Open Cross-Project Dashboard project details');
+ });
+
+ it('should expose the project status to assistive technology', () => {
+ const status: HTMLSpanElement = fixture.nativeElement.querySelector('.project-card__status');
+
+ expect(status.textContent.replace(/\s+/g, ' ').trim()).toBe('Project status: In progress');
});
it('should handle missing optional fields', () => {
@@ -61,5 +67,8 @@ describe('ProjectCardComponent', () => {
expect(text).toContain('Sample Project');
expect(text).toContain('Completed');
expect(text).toContain('Core details are still displayed.');
+
+ const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a');
+ expect(link.getAttribute('aria-label')).toBe('Open Sample Project project details');
});
});
diff --git a/src/app/common/project-card/project-card.component.ts b/src/app/common/project-card/project-card.component.ts
index fa86b34983..89e0704e66 100644
--- a/src/app/common/project-card/project-card.component.ts
+++ b/src/app/common/project-card/project-card.component.ts
@@ -17,8 +17,4 @@ export interface ProjectCardData {
})
export class ProjectCardComponent {
@Input() project!: ProjectCardData;
-
- get hasOptionalDetails(): boolean {
- return !!this.project?.unitCode || !!this.project?.description;
- }
}