From f335fcb63149c69be5312dda2bed51901e40ccc0 Mon Sep 17 00:00:00 2001 From: Naomi Date: Sun, 19 May 2024 07:16:01 +0000 Subject: [PATCH] feat: add testimonials (#10) Closes #4 Reviewed-on: https://codeberg.org/nhcarrigan/portfolio/pulls/10 Co-authored-by: Naomi Co-committed-by: Naomi --- src/app/config/Testimonials.ts | 43 +++++++++++ src/app/home/home.component.css | 22 ++++++ src/app/home/home.component.html | 23 +++++- src/app/home/home.component.spec.ts | 20 +++++ src/app/home/home.component.ts | 13 +++- src/app/review/review.component.css | 73 +++++++++++++++++++ src/app/review/review.component.html | 11 +++ src/app/review/review.component.spec.ts | 53 ++++++++++++++ src/app/review/review.component.ts | 36 +++++++++ .../testimonials/testimonials.component.css | 5 ++ .../testimonials/testimonials.component.html | 10 +++ .../testimonials.component.spec.ts | 38 ++++++++++ .../testimonials/testimonials.component.ts | 19 +++++ 13 files changed, 364 insertions(+), 2 deletions(-) create mode 100644 src/app/config/Testimonials.ts create mode 100644 src/app/review/review.component.css create mode 100644 src/app/review/review.component.html create mode 100644 src/app/review/review.component.spec.ts create mode 100644 src/app/review/review.component.ts create mode 100644 src/app/testimonials/testimonials.component.css create mode 100644 src/app/testimonials/testimonials.component.html create mode 100644 src/app/testimonials/testimonials.component.spec.ts create mode 100644 src/app/testimonials/testimonials.component.ts diff --git a/src/app/config/Testimonials.ts b/src/app/config/Testimonials.ts new file mode 100644 index 0000000..37a8d01 --- /dev/null +++ b/src/app/config/Testimonials.ts @@ -0,0 +1,43 @@ +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import { faLinkedin } from "@fortawesome/free-brands-svg-icons"; + +export const Testimonials: { + name: string; + date: Date; + content: string; + sourceIcon: IconDefinition; + sourceUrl: string; +}[] = [ + { + name: "Eddie Jaoude", + date: new Date("June 30, 2023"), + content: + "Naomi has done a fantastic job in creating Becca Bot, which is an integral part in managing the EddieHub Discord Community. As founder of EddieHub, Naomi is super helpful to all Community members and an excellent moderator, from our text channels to audio calls and live streams. Naomi demonstrates an excellent technical knowledge and is always keen to share this with the community.", + sourceIcon: faLinkedin, + sourceUrl: "https://www.linkedin.com/in/naomi-lgbt/details/recommendations/" + }, + { + name: "Danny Thompson", + date: new Date("July 6 2023"), + content: + "If you need a problem solver, look at Naomi. Naomi is a fantastic part of the online tech community by teaching and offering help to beginners on their journeys into tech. She has created some great solutions and is a consistent learner. Naomi has led initiatives using Javascript and front-end technologies to produce finished products within a volunteer position. Highly recommend Naomi to any team.", + sourceIcon: faLinkedin, + sourceUrl: "https://www.linkedin.com/in/naomi-lgbt/details/recommendations/" + }, + { + name: "Francez Urmatan", + date: new Date("May 2 2024"), + content: + "Naomi is an absolute trailblazer, and is an amazing person to work with! Naomi is humorous and also has an amazing attitude to work with. Her ability to solve complex problems efficiently astounds me. Not only does she demonstrate outstanding technical knowledge, but also does an amazing job at elucidating her needs as an engineer. She is a very warm person and quite easy to work with. Naomi is immensely perceptive and very calculated with what she does. Naomi would make an excellent addition to any company that is lucky enough to hire her!", + sourceIcon: faLinkedin, + sourceUrl: "https://www.linkedin.com/in/naomi-lgbt/details/recommendations/" + }, + { + name: "Katey Berry", + date: new Date("May 14 2024"), + content: + "I've worked alongside Naomi on a number of projects, and it is always a blessing to have her on the team. She is knowledgable, reliable, and always willing to jump in with creative and efficient engineering solutions to complex workflow problems. Naomi is also such a patient teacher, effectively explaining how things work and enabling others to become more independent. I always look forward to working with Naomi, and recommend you work with her if you have the opportunity!", + sourceIcon: faLinkedin, + sourceUrl: "https://www.linkedin.com/in/naomi-lgbt/details/recommendations/" + } +]; diff --git a/src/app/home/home.component.css b/src/app/home/home.component.css index 13592ca..28bfce4 100644 --- a/src/app/home/home.component.css +++ b/src/app/home/home.component.css @@ -12,6 +12,28 @@ section { margin-bottom: 1rem; } +button { + border: 2px solid var(--text); + background-color: var(--bg); + color: var(--text); + font-size: 1.5rem; + cursor: pointer; +} + +button:disabled { + background-color: var(--text); + color: var(--bg); + cursor: not-allowed; +} + +.buttons { + width: 100%; + max-width: 1200px; + display: flex; + justify-content: space-evenly; + margin: auto; +} + .smaller { font-size: 1rem; max-width: 1125px; diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index f40f3fb..75d8d0e 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -20,6 +20,27 @@
+
+ + + + +
- + +
diff --git a/src/app/home/home.component.spec.ts b/src/app/home/home.component.spec.ts index c024988..e113182 100644 --- a/src/app/home/home.component.spec.ts +++ b/src/app/home/home.component.spec.ts @@ -33,4 +33,24 @@ describe("HomeComponent", () => { const logos = compiled.querySelectorAll("app-logo"); expect(logos).toHaveSize(Logos.length); }); + + it("should render the timeline view", () => { + component.changeView("resume"); + fixture.detectChanges(); + compiled = fixture.nativeElement; + const timeline = compiled.querySelector("app-timeline"); + expect(timeline).toBeDefined(); + const testimonials = compiled.querySelector("app-testimonials"); + expect(testimonials).toBeNull(); + }); + + it("should render the testimonials view", () => { + component.changeView("testimonials"); + fixture.detectChanges(); + compiled = fixture.nativeElement; + const timeline = compiled.querySelector("app-timeline"); + expect(timeline).toBeNull(); + const testimonials = compiled.querySelector("app-testimonials"); + expect(testimonials).toBeDefined(); + }); }); diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index c6dd290..5b48c40 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -6,6 +6,7 @@ import { Socials } from "../config/Socials"; import { JobComponent } from "../job/job.component"; import { LogoComponent } from "../logo/logo.component"; import { SocialComponent } from "../social/social.component"; +import { TestimonialsComponent } from "../testimonials/testimonials.component"; import { TimelineComponent } from "../timeline/timeline.component"; /** @@ -19,7 +20,8 @@ import { TimelineComponent } from "../timeline/timeline.component"; SocialComponent, JobComponent, LogoComponent, - TimelineComponent + TimelineComponent, + TestimonialsComponent ], templateUrl: "./home.component.html", styleUrl: "./home.component.css" @@ -27,4 +29,13 @@ import { TimelineComponent } from "../timeline/timeline.component"; export class HomeComponent { public socials = Socials; public logos = Logos; + public view: "resume" | "testimonials" | "certifications" | "stats" = + "testimonials"; + + /** + * @param {string} view The view to load. + */ + changeView(view: typeof this.view) { + this.view = view; + } } diff --git a/src/app/review/review.component.css b/src/app/review/review.component.css new file mode 100644 index 0000000..6a41603 --- /dev/null +++ b/src/app/review/review.component.css @@ -0,0 +1,73 @@ +article { + border: 2px solid var(--text); + margin: 10px; + padding: 10px; + position: relative; + border-radius: 10px; +} + +article::after { + content: ""; + position: absolute; + border-style: solid; + border-width: 15px 0 15px 15px; + border-color: transparent transparent transparent var(--text); + top: 50%; + right: -15px; + transform: translateY(-50%); +} + +.green { + border-color: #8cff98; + color: #8cff98; +} + +.green article { + border-color: #8cff98; +} + +.green a { + color: #8cff98; +} + +.green article::after { + border-color: transparent transparent transparent #8cff98; +} + +.review { + margin: auto; + max-width: 1200px; + display: grid; + grid-template-columns: 4fr 1fr; + align-items: center; +} + +.attrib p, +.attrib { + font-size: 1.25rem; + text-decoration: none; +} + +@media screen and (max-width: 600px) { + article::after { + content: ""; + position: absolute; + border-style: solid; + border-width: 15px 15px 0 15px; + border-color: var(--text) transparent transparent transparent; + bottom: -15px; + left: 50%; + right: auto; + top: auto; + transform: translateX(-50%); + } + + .green article::after { + border-color: #8cff98 transparent transparent transparent; + } + + .review { + grid-template-columns: 100%; + grid-template-rows: auto auto; + } +} diff --git a/src/app/review/review.component.html b/src/app/review/review.component.html new file mode 100644 index 0000000..34efa36 --- /dev/null +++ b/src/app/review/review.component.html @@ -0,0 +1,11 @@ +
+
{{ content }}
+ +

{{ name }}

+
+
diff --git a/src/app/review/review.component.spec.ts b/src/app/review/review.component.spec.ts new file mode 100644 index 0000000..3f06ce0 --- /dev/null +++ b/src/app/review/review.component.spec.ts @@ -0,0 +1,53 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { Testimonials } from "../config/Testimonials"; + +import { ReviewComponent } from "./review.component"; + +describe("ReviewComponent", () => { + let component: ReviewComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ReviewComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(ReviewComponent); + component = fixture.componentInstance; + }); + + for (const expected of Testimonials) { + it(`should parse ${expected.name} properties correctly`, () => { + component.review = expected; + fixture.detectChanges(); + expect(component.review).toBe(expected); + expect(component.name).toBe(expected.name); + expect(component.sourceIcon).toEqual(expected.sourceIcon); + expect(component.sourceUrl).toContain(expected.sourceUrl); + expect(component.content).toBe(expected.content); + }); + + it(`should render ${expected.name} correctly`, () => { + component.review = expected; + fixture.detectChanges(); + compiled = fixture.nativeElement; + const link = compiled.querySelector("a"); + expect(link?.href).toContain(expected.sourceUrl); + expect(link?.target).toBe("_blank"); + expect(link?.rel).toBe("noopener noreferrer"); + + const content = compiled.querySelector("article"); + expect(content?.textContent?.trim()).toBe(expected.content); + const credit = compiled.querySelector("p"); + expect(credit?.textContent?.trim()).toBe(expected.name); + const icon = compiled.querySelector("fa-icon"); + const svg = icon?.querySelector("svg"); + const path = svg?.querySelector("path"); + expect(path?.getAttribute("d")).toBe( + expected.sourceIcon.icon[4] as string + ); + }); + } +}); diff --git a/src/app/review/review.component.ts b/src/app/review/review.component.ts new file mode 100644 index 0000000..f286a22 --- /dev/null +++ b/src/app/review/review.component.ts @@ -0,0 +1,36 @@ +import { Component, Input, OnInit } from "@angular/core"; +import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; + +import { Testimonials } from "../config/Testimonials"; + +/** + * + */ +@Component({ + selector: "app-review", + standalone: true, + imports: [FontAwesomeModule], + templateUrl: "./review.component.html", + styleUrl: "./review.component.css" +}) +export class ReviewComponent implements OnInit { + @Input() review: (typeof Testimonials)[number] = {} as never; + @Input() even = false; + public name = ""; + public content = ""; + public sourceIcon: IconDefinition = {} as never; + public sourceUrl = ""; + public class = "review"; + + /** + * + */ + ngOnInit(): void { + this.name = this.review.name; + this.content = this.review.content; + this.sourceIcon = this.review.sourceIcon; + this.sourceUrl = this.review.sourceUrl; + this.class = this.even ? "review green" : "review"; + } +} diff --git a/src/app/testimonials/testimonials.component.css b/src/app/testimonials/testimonials.component.css new file mode 100644 index 0000000..83b9fca --- /dev/null +++ b/src/app/testimonials/testimonials.component.css @@ -0,0 +1,5 @@ +.desc { + max-width: 1200px; + margin: auto; + font-size: 1.5rem; +} diff --git a/src/app/testimonials/testimonials.component.html b/src/app/testimonials/testimonials.component.html new file mode 100644 index 0000000..b9f5d40 --- /dev/null +++ b/src/app/testimonials/testimonials.component.html @@ -0,0 +1,10 @@ +

Testimonials

+

+ Here's what people have to say about our work! If you want to leave your own, + you can do so through LinkedIn, TopMate, or even an issue on this repository! +

+ diff --git a/src/app/testimonials/testimonials.component.spec.ts b/src/app/testimonials/testimonials.component.spec.ts new file mode 100644 index 0000000..044449d --- /dev/null +++ b/src/app/testimonials/testimonials.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { Testimonials } from "../config/Testimonials"; + +import { TestimonialsComponent } from "./testimonials.component"; + +describe("TestimonialsComponent", () => { + let component: TestimonialsComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestimonialsComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(TestimonialsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should have expected properties", () => { + expect(component.testimonials).toEqual( + Testimonials.sort((a, b) => b.date.getTime() - a.date.getTime()) + ); + }); + + it("should render reviews correctly", () => { + compiled = fixture.nativeElement; + const reviews = compiled.querySelectorAll("app-review"); + expect(reviews).toHaveSize(Testimonials.length); + for (let i = 0; i < component.testimonials.length; i++) { + const review = reviews[i]; + const content = review.querySelector("article"); + expect(content?.textContent).toBe(component.testimonials[i].content); + } + }); +}); diff --git a/src/app/testimonials/testimonials.component.ts b/src/app/testimonials/testimonials.component.ts new file mode 100644 index 0000000..caedd4b --- /dev/null +++ b/src/app/testimonials/testimonials.component.ts @@ -0,0 +1,19 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; + +import { Testimonials } from "../config/Testimonials"; +import { ReviewComponent } from "../review/review.component"; + +/** */ +@Component({ + selector: "app-testimonials", + standalone: true, + imports: [CommonModule, ReviewComponent], + templateUrl: "./testimonials.component.html", + styleUrl: "./testimonials.component.css" +}) +export class TestimonialsComponent { + public testimonials = Testimonials.sort( + (a, b) => b.date.getTime() - a.date.getTime() + ); +}