Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/app/common/project-card/project-card.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<article class="project-card" *ngIf="project">
<div class="project-card__header">
<div>
<p class="project-card__unit" *ngIf="project.unitCode">{{ project.unitCode }}</p>
<h3 class="project-card__title">{{ project.title }}</h3>
</div>

<span class="project-card__status" aria-label="Project status">
{{ project.status }}
</span>
</div>

<p class="project-card__summary">
{{ project.progressSummary }}
</p>

<p class="project-card__description" *ngIf="project.description">
{{ project.description }}
</p>

<a
class="project-card__link"
[href]="project.destinationUrl"
aria-label="Open project details"
>
Open project
</a>
</article>
65 changes: 65 additions & 0 deletions src/app/common/project-card/project-card.component.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
64 changes: 64 additions & 0 deletions src/app/common/project-card/project-card.component.spec.ts
Original file line number Diff line number Diff line change
@@ -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<ProjectCardComponent>;

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.');
});
});
23 changes: 23 additions & 0 deletions src/app/common/project-card/project-card.component.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 4 additions & 0 deletions src/app/doubtfire-angular.module.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -566,6 +569,7 @@ const DEFAULT_TOOLTIP_OPTIONS: MatTooltipDefaultOptions = {
CreateNewUnitModalContentComponent,
TiiActionLogComponent,
FChipComponent,
ProjectCardComponent,
UnitCodeComponent,
NewTeachingPeriodDialogComponent,
FileViewerComponent,
Expand Down