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.unitCode }}

+

{{ project.title }}

+
+ + + {{ project.status }} + +
+ +

+ {{ 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.unitCode }}

-

{{ project.title }}

-
+@if (project) { +
+
+
+ @if (project.unitCode) { +

{{ project.unitCode }}

+ } +

{{ project.title }}

+
- - {{ project.status }} - -
+ + {{ project.status }} + +
-

- {{ 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; - } }