+
-
{pagedCreators.map(creator => (
-
+
))}
@@ -560,7 +588,7 @@ function LandingPage() {
)}
{pagedCreators.map(creator => (
-
+
))}
@@ -806,6 +834,7 @@ function LandingPage() {
+
-
+
);
}
diff --git a/src/utils/__tests__/handleDisplay.utils.test.ts b/src/utils/__tests__/handleDisplay.utils.test.ts
new file mode 100644
index 0000000..e93e0e1
--- /dev/null
+++ b/src/utils/__tests__/handleDisplay.utils.test.ts
@@ -0,0 +1,46 @@
+import { describe, expect, it } from 'vitest';
+import { formatCreatorHandle } from '../handleDisplay.utils';
+
+describe('formatCreatorHandle', () => {
+ it('lowercases mixed-case handles and prepends @', () => {
+ expect(formatCreatorHandle('ARivers')).toBe('@arivers');
+ expect(formatCreatorHandle('Schen_Dev')).toBe('@schen_dev');
+ });
+
+ it('strips an existing leading @ before re-prepending', () => {
+ expect(formatCreatorHandle('@ARivers')).toBe('@arivers');
+ expect(formatCreatorHandle('@@nope')).toBe('@@nope'); // only one @ stripped
+ });
+
+ it('trims surrounding whitespace', () => {
+ expect(formatCreatorHandle(' ARivers ')).toBe('@arivers');
+ expect(formatCreatorHandle(' @ARivers ')).toBe('@arivers');
+ });
+
+ it('returns an empty string for empty / whitespace / nullish input', () => {
+ expect(formatCreatorHandle('')).toBe('');
+ expect(formatCreatorHandle(' ')).toBe('');
+ expect(formatCreatorHandle(null)).toBe('');
+ expect(formatCreatorHandle(undefined)).toBe('');
+ });
+
+ it('returns an empty string when the input is just an @', () => {
+ // A lone @ implies the user forgot to type their handle — no point
+ // rendering "@" alone on a card, callers can fall back to a placeholder.
+ expect(formatCreatorHandle('@')).toBe('');
+ expect(formatCreatorHandle('@ ')).toBe('');
+ });
+
+ it('is idempotent: formatting an already-formatted handle is a no-op', () => {
+ expect(formatCreatorHandle(formatCreatorHandle('ARivers'))).toBe('@arivers');
+ });
+
+ it('does not modify the underlying string the caller passes in', () => {
+ // (Strings are immutable in JS, so this is really about not having
+ // side effects on, e.g., trimming the original via mutation — but the
+ // invariant matters: callers must keep the raw value for equality.)
+ const raw = 'ARivers';
+ formatCreatorHandle(raw);
+ expect(raw).toBe('ARivers');
+ });
+});
diff --git a/src/utils/handleDisplay.utils.ts b/src/utils/handleDisplay.utils.ts
new file mode 100644
index 0000000..4d5ebef
--- /dev/null
+++ b/src/utils/handleDisplay.utils.ts
@@ -0,0 +1,26 @@
+/**
+ * Display-only normalisation for creator handles (issue #298).
+ *
+ * Handles come from a few different sources — the API's `instructorId`, the
+ * user-provided `socialHandle`, and search input — with inconsistent casing
+ * and an optional leading "@". This helper produces a single display form so
+ * the same creator's handle renders the same way on the card, the profile
+ * header, and anywhere else we surface it.
+ *
+ * Rule: strip a leading "@", trim whitespace, lowercase, then prepend a
+ * single "@". Empty or whitespace-only input becomes an empty string so
+ * callers can decide whether to fall back to a placeholder.
+ *
+ * IMPORTANT: this is **display only**. Callers must keep using the raw
+ * stored value for equality checks, URL construction, etc.; passing the
+ * formatted value back to the API would lose the original casing.
+ */
+export const formatCreatorHandle = (raw: string | null | undefined): string => {
+ if (raw == null) return '';
+ const trimmed = raw.trim();
+ if (trimmed === '') return '';
+ const withoutLeadingAt = trimmed.startsWith('@') ? trimmed.slice(1) : trimmed;
+ const normalised = withoutLeadingAt.trim().toLowerCase();
+ if (normalised === '') return '';
+ return `@${normalised}`;
+};