diff --git a/src/app.controller.ts b/src/app.controller.ts index 187be1c..deccbc9 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -5,14 +5,17 @@ import { SkillsService } from "@/skills/skills.service"; import { ExperiencesService } from "@/experiences/experiences.service"; import { SkillDto } from "@/skills/skills.types"; import { ExperienceDto } from "@/experiences/experiences.types"; +import { EducationDto } from "@/education/education.types"; +import { EducationService } from "@/education/education.service"; @Controller() -@ApiExtraModels(SkillDto, ExperienceDto) +@ApiExtraModels(SkillDto, ExperienceDto, EducationDto) export class AppController { constructor( private readonly appService: AppService, private readonly skillsService: SkillsService, - private readonly experiencesService: ExperiencesService + private readonly experiencesService: ExperiencesService, + private readonly educationService: EducationService ) {} @Get() @@ -48,4 +51,18 @@ export class AppController { getExperiences(): readonly ExperienceDto[] { return this.experiencesService.getMany(); } + + @Get("education") + @ApiTags("Education") + @ApiOkResponse({ + description: "Returns a list of received education", + schema: { + items: { + $ref: getSchemaPath(EducationDto), + }, + }, + }) + getEducation(): readonly EducationDto[] { + return this.educationService.getMany(); + } } diff --git a/src/app.module.ts b/src/app.module.ts index b859374..69e0b8a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,10 +3,11 @@ import { AppController } from "@/app.controller"; import { AppService } from "@/app.service"; import { SkillsService } from "@/skills/skills.service"; import { ExperiencesService } from "@/experiences/experiences.service"; +import { EducationService } from "@/education/education.service"; @Module({ imports: [], controllers: [AppController], - providers: [AppService, SkillsService, ExperiencesService], + providers: [AppService, SkillsService, ExperiencesService, EducationService], }) export class AppModule {} diff --git a/src/cvdocs/education.ts b/src/cvdocs/education.ts new file mode 100644 index 0000000..5167bf9 --- /dev/null +++ b/src/cvdocs/education.ts @@ -0,0 +1,59 @@ +import { EducationType } from "@/education/education.types"; +import { DocumentBuilder } from "@nestjs/swagger"; +import { RedocOptions } from "@juicyllama/nestjs-redoc"; +import { education } from "@/education/education"; + +const formatEducation = (education: EducationType) => { + const period = `${formatDate(education.startDate)} - ${ + education.endDate ? formatDate(education.endDate) : "present" + }`; + const institute = education.url + ? `${education.institute}` + : education.institute; + const description = education.description.split("\n").join("

"); + + return `

${institute}, ${education.city}
+${education.level} - ${education.course}
+
+

${description}

`; +}; + +const formatDate = (date: Date, locale: string | string[] = "en-GB"): string => { + const dateFormatter = new Intl.DateTimeFormat(locale, { + dateStyle: "long", + }); + const dateParts = dateFormatter.formatToParts(date).reduce>( + (result, datePart: Intl.DateTimeFormatPart) => { + if (datePart.type !== "literal") { + result[datePart.type] = datePart.value; + } + + return result; + }, + { day: "", month: "", year: "" } + ); + + return `${dateParts.month} ${dateParts.year}`; +}; + +export const addEducation = ( + document: DocumentBuilder, + count: number = 5, + redocOptions: RedocOptions +): DocumentBuilder => { + let educationTagGroup = redocOptions.tagGroups.find((tagGroup) => tagGroup.name === "Education"); + if (!educationTagGroup) { + educationTagGroup = { + name: "Education", + tags: ["Education"], + }; + redocOptions.tagGroups.push(educationTagGroup); + } + + education.slice(0, count).forEach((education) => { + educationTagGroup.tags.push(education.institute); + document.addTag(education.institute, formatEducation(education)); + }); + + return document; +}; diff --git a/src/cvdocs/openApi.ts b/src/cvdocs/openApi.ts index a33c466..e1ddedd 100644 --- a/src/cvdocs/openApi.ts +++ b/src/cvdocs/openApi.ts @@ -4,6 +4,7 @@ import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; import { addExperiences } from "@/cvdocs/experience"; import { addSkills } from "@/cvdocs/skills"; import { addIntro } from "@/cvdocs/intro"; +import { addEducation } from "@/cvdocs/education"; const apiDocument = ( options: { @@ -80,6 +81,7 @@ export default async (app: INestApplication) => { addIntro(config, redocOptions); addSkills(config, 5, redocOptions); addExperiences(config, 3, redocOptions); + addEducation(config, 2, redocOptions); await initDocs({ app, diff --git a/src/education/education.service.spec.ts b/src/education/education.service.spec.ts new file mode 100644 index 0000000..1d06d4f --- /dev/null +++ b/src/education/education.service.spec.ts @@ -0,0 +1,32 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { EducationService } from "src/education/education.service"; + +describe("SkillsService", () => { + let service: EducationService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [EducationService], + }).compile(); + + service = module.get(EducationService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + describe("getMany method", () => { + it("should be defined", () => { + expect(service.getMany).toBeDefined(); + }); + + it("should return an array", () => { + expect(Array.isArray(service.getMany())).toBe(true); + }); + + it("should return an array", () => { + expect(Array.isArray(service.getMany())).toBe(true); + }); + }); +}); diff --git a/src/education/education.service.ts b/src/education/education.service.ts new file mode 100644 index 0000000..89b6006 --- /dev/null +++ b/src/education/education.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from "@nestjs/common"; +import { EducationDto, EducationType } from "@/education/education.types"; +import { education } from "@/education/education"; + +@Injectable() +export class EducationService { + private readonly education: readonly EducationDto[]; + + constructor() { + this.education = EducationDto.asDto(education).sort((educationA, educationB) => + educationA.startDate < educationB.startDate ? 1 : -1 + ); + } + + getMany(filter?: Partial>) { + const filtersValues = Object.entries(filter ?? {}).map(([key, filterValue]) => [ + key, + new RegExp(filterValue, "i"), + ]) as Array<[keyof Omit, RegExp]>; + if (!filter || filtersValues.length === 0) { + return this.education; + } + + return this.education.filter((education) => + filtersValues.some(([key, filterValue]) => filterValue.test(education[key])) + ); + } +} diff --git a/src/education/education.ts b/src/education/education.ts new file mode 100644 index 0000000..911b7f7 --- /dev/null +++ b/src/education/education.ts @@ -0,0 +1,47 @@ +import { EducationType } from "@/education/education.types"; + +export const education: EducationType[] = [ + { + institute: "Hogeschool Utrecht", + city: "Utrecht", + url: "https://hu.nl", + course: "Mediatechnologie", + level: "HBO", + startDate: new Date(2010, 8), + endDate: new Date(2015, 9), + description: `My time at Hogeschool Utrecht was a period of academic and personal growth. +While pursuing my Media Technology degree, I took a gap year to co-found and serve as treasurer of Sv. Ingenium, a university-affiliated student society. This experience sharpened my organizational, communication, and leadership skills, proving invaluable throughout my studies and beyond. +Additionally, active participation in projects and presentations enhanced my presentation skills and public speaking confidence.`, + }, + { + institute: "Mediacollege Amsterdam", + city: "Amsterdam", + url: "https://www.ma-web.nl/", + course: "Interactive Design & Media technology", + level: "MBO 4", + startDate: new Date(2006, 8), + endDate: new Date(2010, 5), + description: `My time at Mediacollege Amsterdam began with Interactive Design, but programming quickly captured my interest. ActionScript 2, now a relic of the past, marked my introduction to the world of programming, while PHP and JavaScript truly sparked my passion. +With help of my teachers and a careful consideration of course, I transitioned towards Media Technology, paving the way for my future in software development.`, + }, + { + institute: "Clusius college", + city: "Castricum", + url: "https://www.vonknh.nl/vmbo/castricum", + course: "General secondary education", + level: "VMBO - GL", + startDate: new Date(2002, 8), + endDate: new Date(2006, 5), + description: "", + }, + { + institute: "Watermolen", + city: "Koog aan de zaan", + url: "https://www.obsdewatermolen.nl/", + course: "General primary education", + level: "Primary education", + startDate: new Date(1994, 8), + endDate: new Date(2002, 5), + description: "", + }, +] satisfies EducationType[]; diff --git a/src/education/education.types.ts b/src/education/education.types.ts new file mode 100644 index 0000000..d261a72 --- /dev/null +++ b/src/education/education.types.ts @@ -0,0 +1,46 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { DtoClass } from "@/common/dtoClass.factory"; + +export type EducationType = { + institute: string; + city: string; + url?: string; + course: string; + level: string; + startDate: Date; + endDate: Date; + description: string; +}; + +export class EducationDto extends DtoClass() implements EducationType { + @ApiProperty() + readonly institute: string; + @ApiProperty() + readonly city: string; + @ApiProperty() + readonly url?: string; + @ApiProperty() + readonly course: string; + @ApiProperty() + readonly level: string; + @ApiProperty() + readonly startDate: Date; + @ApiProperty() + readonly endDate: Date; + @ApiProperty() + readonly description: string; + + constructor(education: EducationType) { + super(education); + Object.assign(this, { + institute: education.institute, + city: education.city, + url: education.url, + course: education.course, + level: education.level, + startDate: education.startDate, + endDate: education.endDate, + description: education.description, + }); + } +}