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..5300e34f54 --- /dev/null +++ b/src/app/common/project-card/project-card.component.html @@ -0,0 +1,34 @@ +@if (project) { +
+
+
+ @if (project.unitCode) { +

{{ project.unitCode }}

+ } +

{{ project.title }}

+
+ + + Project status: {{ project.status }} + +
+ +

+ {{ project.progressSummary }} +

+ + @if (project.description) { +

+ {{ project.description }} +

+ } + + + Open project + +
+} 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..e90278716d --- /dev/null +++ b/src/app/common/project-card/project-card.component.scss @@ -0,0 +1,77 @@ +.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__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; + 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; + } +} 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..5d8a798c81 --- /dev/null +++ b/src/app/common/project-card/project-card.component.spec.ts @@ -0,0 +1,74 @@ +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 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', () => { + fixture.componentRef.setInput('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.'); + + 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 new file mode 100644 index 0000000000..89e0704e66 --- /dev/null +++ b/src/app/common/project-card/project-card.component.ts @@ -0,0 +1,20 @@ +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', + standalone: false, + templateUrl: './project-card.component.html', + styleUrls: ['./project-card.component.scss'], +}) +export class ProjectCardComponent { + @Input() project!: ProjectCardData; +} diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index c4abd459bc..1502bc99b3 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -218,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'; @@ -566,6 +567,7 @@ const DEFAULT_TOOLTIP_OPTIONS: MatTooltipDefaultOptions = { CreateNewUnitModalContentComponent, TiiActionLogComponent, FChipComponent, + ProjectCardComponent, UnitCodeComponent, NewTeachingPeriodDialogComponent, FileViewerComponent,