feat: add testimonials (#10)

Closes #4

Reviewed-on: https://codeberg.org/nhcarrigan/portfolio/pulls/10
Co-authored-by: Naomi <commits@nhcarrigan.com>
Co-committed-by: Naomi <commits@nhcarrigan.com>
This commit is contained in:
Naomi Carrigan 2024-05-19 07:16:01 +00:00 committed by Naomi Carrigan
parent b909f4666c
commit f335fcb631
13 changed files with 364 additions and 2 deletions

View File

@ -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/"
}
];

View File

@ -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;

View File

@ -20,6 +20,27 @@
</div>
</section>
<hr />
<div class="buttons">
<button (click)="changeView('resume')" [disabled]="view === 'resume'">
Resume
</button>
<button
(click)="changeView('testimonials')"
[disabled]="view === 'testimonials'"
>
Testimonials
</button>
<button
(click)="changeView('certifications')"
[disabled]="view === 'certifications'"
>
Certifications
</button>
<button (click)="changeView('stats')" [disabled]="view === 'stats'">
Stats
</button>
</div>
<section>
<app-timeline></app-timeline>
<app-timeline *ngIf="view === 'resume'"></app-timeline>
<app-testimonials *ngIf="view === 'testimonials'"></app-testimonials>
</section>

View File

@ -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();
});
});

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,11 @@
<div class="{{ class }}">
<article>{{ content }}</article>
<a
href="{{ sourceUrl }}"
target="_blank"
rel="noopener noreferrer"
class="attrib"
>
<p><fa-icon [icon]="sourceIcon"></fa-icon> {{ name }}</p>
</a>
</div>

View File

@ -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<ReviewComponent>;
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
);
});
}
});

View File

@ -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";
}
}

View File

@ -0,0 +1,5 @@
.desc {
max-width: 1200px;
margin: auto;
font-size: 1.5rem;
}

View File

@ -0,0 +1,10 @@
<h2>Testimonials</h2>
<p class="desc">
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!
</p>
<app-review
*ngFor="let review of testimonials"
[review]="review"
[even]="testimonials.indexOf(review) % 2 === 0"
></app-review>

View File

@ -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<TestimonialsComponent>;
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);
}
});
});

View File

@ -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()
);
}