Skip to content

Commit 692a01a

Browse files
committed
Add reusable project card component
1 parent 20d1f38 commit 692a01a

5 files changed

Lines changed: 181 additions & 0 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<article class="project-card" *ngIf="project">
2+
<div class="project-card__header">
3+
<div>
4+
<p class="project-card__unit" *ngIf="project.unitCode">{{ project.unitCode }}</p>
5+
<h3 class="project-card__title">{{ project.title }}</h3>
6+
</div>
7+
8+
<span class="project-card__status" aria-label="Project status">
9+
{{ project.status }}
10+
</span>
11+
</div>
12+
13+
<p class="project-card__summary">
14+
{{ project.progressSummary }}
15+
</p>
16+
17+
<p class="project-card__description" *ngIf="project.description">
18+
{{ project.description }}
19+
</p>
20+
21+
<a
22+
class="project-card__link"
23+
[href]="project.destinationUrl"
24+
aria-label="Open project details"
25+
>
26+
Open project
27+
</a>
28+
</article>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
.project-card {
2+
border: 1px solid #d6d6d6;
3+
border-radius: 12px;
4+
padding: 1rem;
5+
background: #ffffff;
6+
display: flex;
7+
flex-direction: column;
8+
gap: 0.75rem;
9+
max-width: 420px;
10+
}
11+
12+
.project-card__header {
13+
display: flex;
14+
justify-content: space-between;
15+
gap: 1rem;
16+
align-items: flex-start;
17+
}
18+
19+
.project-card__unit {
20+
margin: 0 0 0.25rem;
21+
font-size: 0.8rem;
22+
font-weight: 600;
23+
color: #555555;
24+
}
25+
26+
.project-card__title {
27+
margin: 0;
28+
font-size: 1.1rem;
29+
line-height: 1.3;
30+
}
31+
32+
.project-card__status {
33+
border-radius: 999px;
34+
padding: 0.25rem 0.6rem;
35+
font-size: 0.8rem;
36+
font-weight: 600;
37+
background: #eeeeee;
38+
white-space: nowrap;
39+
}
40+
41+
.project-card__summary,
42+
.project-card__description {
43+
margin: 0;
44+
line-height: 1.5;
45+
}
46+
47+
.project-card__link {
48+
align-self: flex-start;
49+
font-weight: 600;
50+
text-decoration: underline;
51+
}
52+
53+
@media (max-width: 520px) {
54+
.project-card {
55+
max-width: 100%;
56+
}
57+
58+
.project-card__header {
59+
flex-direction: column;
60+
}
61+
62+
.project-card__status {
63+
align-self: flex-start;
64+
}
65+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { ProjectCardComponent, ProjectCardData } from './project-card.component';
3+
4+
describe('ProjectCardComponent', () => {
5+
let component: ProjectCardComponent;
6+
let fixture: ComponentFixture<ProjectCardComponent>;
7+
8+
const sampleProject: ProjectCardData = {
9+
title: 'Cross-Project Dashboard',
10+
unitCode: 'CPD-F02',
11+
status: 'In progress',
12+
progressSummary: 'Reusable dashboard project-card component is being prepared.',
13+
description: 'This card summarises one project without hardcoding the dashboard data.',
14+
destinationUrl: '/projects/cpd-f02'
15+
};
16+
17+
beforeEach(async () => {
18+
await TestBed.configureTestingModule({
19+
declarations: [ProjectCardComponent]
20+
}).compileComponents();
21+
22+
fixture = TestBed.createComponent(ProjectCardComponent);
23+
component = fixture.componentInstance;
24+
component.project = sampleProject;
25+
fixture.detectChanges();
26+
});
27+
28+
it('should create the project card component', () => {
29+
expect(component).toBeTruthy();
30+
});
31+
32+
it('should display the supplied project details', () => {
33+
const text = fixture.nativeElement.textContent;
34+
35+
expect(text).toContain('Cross-Project Dashboard');
36+
expect(text).toContain('CPD-F02');
37+
expect(text).toContain('In progress');
38+
expect(text).toContain('Reusable dashboard project-card component is being prepared.');
39+
});
40+
41+
it('should provide an accessible project link', () => {
42+
const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a');
43+
44+
expect(link).toBeTruthy();
45+
expect(link.getAttribute('href')).toBe('/projects/cpd-f02');
46+
expect(link.getAttribute('aria-label')).toBe('Open project details');
47+
});
48+
49+
it('should handle missing optional fields', () => {
50+
component.project = {
51+
title: 'Sample Project',
52+
status: 'Completed',
53+
progressSummary: 'Core details are still displayed.',
54+
destinationUrl: '/projects/sample'
55+
};
56+
57+
fixture.detectChanges();
58+
59+
const text = fixture.nativeElement.textContent;
60+
expect(text).toContain('Sample Project');
61+
expect(text).toContain('Completed');
62+
expect(text).toContain('Core details are still displayed.');
63+
});
64+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Component, Input } from '@angular/core';
2+
3+
export interface ProjectCardData {
4+
title: string;
5+
status: string;
6+
progressSummary: string;
7+
destinationUrl: string;
8+
unitCode?: string;
9+
description?: string;
10+
}
11+
12+
@Component({
13+
selector: 'project-card',
14+
templateUrl: './project-card.component.html',
15+
styleUrls: ['./project-card.component.scss']
16+
})
17+
export class ProjectCardComponent {
18+
@Input() project!: ProjectCardData;
19+
20+
get hasOptionalDetails(): boolean {
21+
return !!this.project?.unitCode || !!this.project?.description;
22+
}
23+
}

‎src/app/doubtfire-angular.module.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,7 @@ const DEFAULT_TOOLTIP_OPTIONS: MatTooltipDefaultOptions = {
566566
CreateNewUnitModalContentComponent,
567567
TiiActionLogComponent,
568568
FChipComponent,
569+
ProjectCardComponent,
569570
UnitCodeComponent,
570571
NewTeachingPeriodDialogComponent,
571572
FileViewerComponent,

0 commit comments

Comments
 (0)