Your profile here
{/* "Working towards it" is a statement about people, not about a
@@ -379,7 +440,7 @@ function FilterGroup({ label, children }: { label: string; children: React.React
const labelId = useId();
return (
-
+
- {person.name}
+ {person.name}
{person.headline ? (
- {person.headline}
+ {person.headline}
) : null}
{person.location ? (
- {person.location}
+ {person.location}
) : null}
@@ -448,31 +509,36 @@ function PractitionerRow({ person }: { person: Profile }) {
No credentials listed.
) : (
)}
diff --git a/src/lib/practitioners.test.ts b/src/lib/practitioners.test.ts
index a367fee..114281c 100644
--- a/src/lib/practitioners.test.ts
+++ b/src/lib/practitioners.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
byCatalogueOrder,
+ byRosterOrder,
countryName,
credentialSource,
hasVerifiedBadge,
@@ -129,6 +130,35 @@ describe("catalogue order", () => {
});
});
+describe("roster order", () => {
+ const numbered = (kind: CatalogueEntry["kind"], label: string, sortOrder: number) => ({
+ ...entry(kind, label),
+ sortOrder,
+ });
+
+ it("leads with certifications rather than interleaving them with the Academy track", () => {
+ const scrambled = [
+ numbered("certification", "Claude Certified Associate - Foundations (CCAO-F)", 0),
+ numbered("course", "Claude 101", 0),
+ numbered("certification", "Claude Certified Architect - Foundations (CCAR-F)", 1),
+ numbered("course", "Claude Code 101", 1),
+ ];
+
+ expect([...scrambled].sort(byRosterOrder).map((item) => item.label)).toEqual([
+ "Claude Certified Associate - Foundations (CCAO-F)",
+ "Claude Certified Architect - Foundations (CCAR-F)",
+ "Claude 101",
+ "Claude Code 101",
+ ]);
+ });
+
+ it("breaks the last tie on the label, so the order is stable rather than merely sorted", () => {
+ const tied = [numbered("course", "Beta", 4), numbered("course", "Alpha", 4)];
+
+ expect([...tied].sort(byRosterOrder).map((item) => item.label)).toEqual(["Alpha", "Beta"]);
+ });
+});
+
describe("what a credential says it came from", () => {
it("names the weight for a certification and the platform for a course", () => {
expect(credentialSource(developerCert)).toBe("Claude Certification");
diff --git a/src/lib/practitioners.ts b/src/lib/practitioners.ts
index 3195486..d37faf6 100644
--- a/src/lib/practitioners.ts
+++ b/src/lib/practitioners.ts
@@ -107,6 +107,34 @@ export function byCatalogueOrder(left: CatalogueEntry, right: CatalogueEntry) {
);
}
+/**
+ * Roster order: certifications before courses, then `sortOrder`, then label.
+ *
+ * **`sortOrder` alone is not an order.** It restarts at 0 per platform — the
+ * seed's own note says so, and `unique (kind, platform, label)` does not
+ * constrain it. A course and an exam collide on every low number. Sorting on
+ * it by itself leaves the collisions to whatever order the rows arrived in,
+ * which for a `select` is whatever Postgres found. The result is the Academy
+ * track interleaved with the exams and led by them, which is precisely the
+ * scrambled reading `sortOrder` exists to prevent.
+ *
+ * Certifications first because that is what someone is looking for in this view; the label
+ * breaks the last tie so the list is stable rather than merely sorted.
+ *
+ * **One copy, used by both halves of the credentials block.** The profile page
+ * draws what somebody holds and, behind the *Not earned* control, what they do
+ * not. Two lists over the same catalogue, on the same screen. A second
+ * comparator is how they came to disagree.
+ */
+
+export function byRosterOrder(left: CatalogueEntry, right: CatalogueEntry) {
+ return (
+ Number(right.kind === "certification") - Number(left.kind === "certification") ||
+ left.sortOrder - right.sortOrder ||
+ left.label.localeCompare(right.label)
+ );
+}
+
/**
* The one line a public surface prints under a credential's label.
*