generated from nhcarrigan/template
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:
parent
b909f4666c
commit
f335fcb631
43
src/app/config/Testimonials.ts
Normal file
43
src/app/config/Testimonials.ts
Normal 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/"
|
||||
}
|
||||
];
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
73
src/app/review/review.component.css
Normal file
73
src/app/review/review.component.css
Normal 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;
|
||||
}
|
||||
}
|
11
src/app/review/review.component.html
Normal file
11
src/app/review/review.component.html
Normal 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>
|
53
src/app/review/review.component.spec.ts
Normal file
53
src/app/review/review.component.spec.ts
Normal 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
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
36
src/app/review/review.component.ts
Normal file
36
src/app/review/review.component.ts
Normal 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";
|
||||
}
|
||||
}
|
5
src/app/testimonials/testimonials.component.css
Normal file
5
src/app/testimonials/testimonials.component.css
Normal file
@ -0,0 +1,5 @@
|
||||
.desc {
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
font-size: 1.5rem;
|
||||
}
|
10
src/app/testimonials/testimonials.component.html
Normal file
10
src/app/testimonials/testimonials.component.html
Normal 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>
|
38
src/app/testimonials/testimonials.component.spec.ts
Normal file
38
src/app/testimonials/testimonials.component.spec.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
19
src/app/testimonials/testimonials.component.ts
Normal file
19
src/app/testimonials/testimonials.component.ts
Normal 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()
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user