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 000000000..053f807bc --- /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 000000000..d32a89c16 --- /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 000000000..4c5d9d704 --- /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 000000000..521a42969 --- /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 c4abd459b..ffc3c9688 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,