From b861b5a3bb9f116e32583cbcd1eeeebe30e22bbe Mon Sep 17 00:00:00 2001 From: Andrii Naidenko Date: Sat, 10 Oct 2026 01:09:14 +0300 Subject: [PATCH] feat(about): Nick Murphy's LinkedIn recommendation first under What clients said, and release 1.10.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The co-founder of Buddy Punch, the client since 2019, wrote a LinkedIn recommendation on 2026-10-09. It now opens "What clients said", verbatim and in its two paragraphs, captioned "Nick Murphy · Co-Founder, Buddy Punch" and "Client through Toptal since 2019 · LinkedIn recommendation". It carries no stars. The stars are each Upwork review's own rating, and the page says so in words ("Rated 5 out of 5.") to screen readers and in /index.md; a LinkedIn recommendation has no rating, so `rating` is optional and an item without one shows no stars and no such sentence. A quote's paragraphs, split on a blank line, render as paragraphs on the page and in the Markdown. Three new unit tests (red first), the e2e test of the reviews rewritten for four quotes. Checks on this tree: format, lint, types, 269 unit tests, e2e 220 passed and 12 skipped. --- e2e/site.spec.ts | 11 +++++++++-- package.json | 2 +- src/components/About.tsx | 21 ++++++++++++++------- src/content/testimonials.ts | 17 +++++++++++++---- src/lib/format.test.ts | 12 +++++++++++- src/lib/format.ts | 8 ++++++++ src/lib/markdown.test.ts | 9 +++++++++ src/lib/markdown.ts | 4 ++-- 8 files changed, 67 insertions(+), 17 deletions(-) diff --git a/e2e/site.spec.ts b/e2e/site.spec.ts index 8b9657b..bf076e6 100644 --- a/e2e/site.spec.ts +++ b/e2e/site.spec.ts @@ -191,9 +191,16 @@ test("shows the current year in the footer, not the year of the build", async ({ await expect(page.locator("footer")).toContainText("© 2031"); }); -test("quotes three repeat clients by name under the numbers, each with a rating", async ({ page }) => { +test("quotes Buddy Punch's co-founder first, with no rating, then three repeat Upwork clients with theirs", async ({ page }) => { await page.goto("/"); - await expect(page.locator("section#about figure")).toHaveCount(3); + const figures = page.locator("section#about figure"); + await expect(figures).toHaveCount(4); + await expect(figures.first().locator("blockquote p")).toHaveCount(2); + const first = figures.first().locator("figcaption"); + await expect(first).toContainText("Nick Murphy · Co-Founder, Buddy Punch"); + await expect(first).toContainText("LinkedIn recommendation"); + await expect(first).not.toContainText("Rated"); + await expect(first).not.toContainText("★"); for (const name of ["Bruce van Zyl", "Chris Robichaud", "Alex Harper"]) { const caption = page.locator("section#about figcaption", { hasText: name }); await expect(caption).toContainText("Rated 5 out of 5."); diff --git a/package.json b/package.json index 3acb09b..735e3c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "naidenko-dev", - "version": "1.9.0", + "version": "1.10.0", "private": true, "scripts": { "dev": "next dev", diff --git a/src/components/About.tsx b/src/components/About.tsx index 588a2bc..f5af1a2 100644 --- a/src/components/About.tsx +++ b/src/components/About.tsx @@ -1,5 +1,6 @@ import { aboutParagraphs, numbers } from "@/content/about"; import { testimonials } from "@/content/testimonials"; +import { quoteParagraphs } from "@/lib/format"; import { Section } from "./Section"; @@ -24,15 +25,21 @@ export function About() { {testimonials.items.map(item => (
  • -
    -

    “{item.quote}”

    +
    + {quoteParagraphs(item.quote).map(paragraph => ( +

    {paragraph}

    + ))}
    - - Rated {item.rating} out of 5. - {item.name} + {item.rating ? ( + <> + + Rated {item.rating} out of 5. + + ) : null} + {item.name} {"project" in item ? · {item.project} : null} {item.hired}
    diff --git a/src/content/testimonials.ts b/src/content/testimonials.ts index 05e5607..435b4ec 100644 --- a/src/content/testimonials.ts +++ b/src/content/testimonials.ts @@ -1,19 +1,28 @@ export interface Testimonial { + /** Paragraphs are separated by a blank line. */ quote: string; name: string; project?: string; hired: string; - /** The client's Upwork rating for that contract. */ - rating: number; + /** The client's Upwork rating for that contract; a LinkedIn recommendation has none. */ + rating?: number; } /** - * Upwork client reviews, verbatim, from the contract pages saved 2026-09-24. Chris Robichaud also - * posted the same words as a LinkedIn recommendation (April 2019). + * Verbatim. Nick Murphy's LinkedIn recommendation of 2026-10-09 (Andrii's screenshot of 2026-10-10 + * in the resume repository's `permissions/`), then Upwork client reviews from the contract pages + * saved 2026-09-24. Chris Robichaud also posted the same words as a LinkedIn recommendation (April + * 2019). */ export const testimonials = { heading: "What clients said", items: [ + { + quote: "Andrii is one of the easiest people I’ve worked with. He is reliable, communicates clearly, and raises problems early instead of letting them surprise you later. When something is unclear, his question comes with a proposed answer, which makes every decision faster. I never have to chase him for status or wonder where things stand.\n\nHe has been a main contributor to our mobile app at Buddy Punch, covering new features, hotfixes and releases, and he takes care with all of it. I’d recommend Andrii to any team looking for a strong mobile developer who is genuinely good to work with.", + name: "Nick Murphy", + project: "Co-Founder, Buddy Punch", + hired: "Client through Toptal since 2019 · LinkedIn recommendation" + }, { quote: "Andrii has been one of our best, most reliable developers on many projects. He works efficiently, communicates effectively, and cares about his quality of work. We will definitely work with him again.", name: "Bruce van Zyl", diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts index 51d9a8a..78531bc 100644 --- a/src/lib/format.test.ts +++ b/src/lib/format.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { compactFigure } from "./format"; +import { compactFigure, quoteParagraphs } from "./format"; describe("compactFigure", () => { it("shortens a store figure and keeps its plus sign", () => { @@ -12,3 +12,13 @@ describe("compactFigure", () => { expect(compactFigure("731")).toBe("731"); }); }); + +describe("quoteParagraphs", () => { + it("opens the quote on the first paragraph and closes it on the last", () => { + expect(quoteParagraphs("One.\n\nTwo.\n\nThree.")).toEqual(["“One.", "Two.", "Three.”"]); + }); + + it("quotes a single paragraph whole", () => { + expect(quoteParagraphs("Only.")).toEqual(["“Only.”"]); + }); +}); diff --git a/src/lib/format.ts b/src/lib/format.ts index 5ba219f..5342c17 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -5,3 +5,11 @@ export function compactFigure(figure: string): string { const plus = figure.endsWith("+") ? "+" : ""; return compact.format(Number(figure.replace(/[^\d.]/g, ""))) + plus; } + +/** A quote's paragraphs (split on a blank line), with the opening mark on the first and the closing one on the last. */ +export function quoteParagraphs(quote: string): string[] { + const paragraphs = quote.split("\n\n"); + paragraphs[0] = `“${paragraphs[0]}`; + paragraphs[paragraphs.length - 1] += "”"; + return paragraphs; +} diff --git a/src/lib/markdown.test.ts b/src/lib/markdown.test.ts index 16f32f9..8aa1d5f 100644 --- a/src/lib/markdown.test.ts +++ b/src/lib/markdown.test.ts @@ -20,6 +20,15 @@ describe("pageMarkdown", () => { expect(md).toContain("(https://www.toptal.com/developers/resume/andrii-naidenko#qjl3b7)"); }); + it("rates only the reviews that came with a rating", () => { + expect(md).toContain("> — Nick Murphy, Co-Founder, Buddy Punch. Client through Toptal since 2019 · LinkedIn recommendation.\n"); + expect(md.match(/Rated 5 out of 5\./g)).toHaveLength(3); + }); + + it("keeps a review's paragraphs inside its quote", () => { + expect(md).toContain("where things stand.\n>\n> He has been a main contributor"); + }); + it("gives no email address and no form, which an agent sent from Toptal would read", () => { expect(md).not.toContain("@naidenko.dev"); expect(md).not.toContain("#contact"); diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts index 441e307..d57d3a5 100644 --- a/src/lib/markdown.ts +++ b/src/lib/markdown.ts @@ -41,9 +41,9 @@ export function pageMarkdown(): string { `## ${testimonials.heading}`, "", ...testimonials.items.flatMap(item => [ - `> ${item.quote}`, + ...item.quote.split("\n\n").flatMap((paragraph, i) => (i ? [">", `> ${paragraph}`] : [`> ${paragraph}`])), ">", - `> — ${item.name}${"project" in item ? `, ${item.project}` : ""}. ${item.hired}. Rated ${item.rating} out of 5.`, + `> — ${item.name}${"project" in item ? `, ${item.project}` : ""}. ${item.hired}.${item.rating ? ` Rated ${item.rating} out of 5.` : ""}`, "" ]), "## Experience",