Skip to content

Commit

Permalink
support react nodes in conjuction function (#311)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcoMadera committed Apr 3, 2024
1 parent 2ba197b commit 6a43347
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 75 deletions.
71 changes: 36 additions & 35 deletions components/ArtistList/ArtistList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Fragment, ReactElement, useRef } from "react";

import Link from "next/link";

import { useTranslations } from "hooks";
import { ITrack, ITrackArtist } from "types/spotify";
import { getIdFromUri } from "utils";
import { conjuction, getIdFromUri } from "utils";

export interface IArtistListProps {
artists: ITrack["artists"];
Expand All @@ -21,46 +22,46 @@ export default function ArtistList({
onClick,
}: Readonly<IArtistListProps>): ReactElement | null {
const artistsRef = useRef<HTMLAnchorElement>(null);
const { locale } = useTranslations();
if (!artists) return null;
const { length } = artists;

const lastArtistIndex = maxArtistsToShow ? maxArtistsToShow - 1 : length - 1;
return (
<span>
{artists.map((artist, i) => {
const id = getArtistId(artist);
if (maxArtistsToShow && i >= maxArtistsToShow) return null;
{conjuction(
artists
.slice(0, maxArtistsToShow ?? artists.length)
.filter((el) => el.name)
.map((artist) => {
const id = getArtistId(artist);

if (!id && artist.name) {
return (
<span key={artist.name} ref={artistsRef} className="ArtistList">
{artist.name}
{i !== lastArtistIndex ? ", " : ""}
</span>
);
}

if (!id) return null;
if (!id) {
return (
<span key={artist.name} ref={artistsRef} className="ArtistList">
{artist.name}
</span>
);
}

return (
<Fragment key={id}>
<Link
href={`/${
artist.type ?? getIdFromUri(artist.uri, "type") ?? "artist"
}/${id}`}
ref={artistsRef}
onClick={(e) => {
e.stopPropagation();
if (onClick) onClick(e);
}}
className="ArtistList"
>
{artist.name}
</Link>
{i !== lastArtistIndex ? ", " : null}
</Fragment>
);
})}
return (
<Fragment key={id}>
<Link
href={`/${
artist.type ?? getIdFromUri(artist.uri, "type") ?? "artist"
}/${id}`}
ref={artistsRef}
onClick={(e) => {
e.stopPropagation();
if (onClick) onClick(e);
}}
className="ArtistList"
>
{artist.name}
</Link>
</Fragment>
);
}),
locale
)}
<style jsx>{`
span :global(.ArtistList) {
color: inherit;
Expand Down
21 changes: 17 additions & 4 deletions components/ArtistList/__tests__/ArtistList.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,29 @@ import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";

import ArtistList, { IArtistListProps } from "components/ArtistList";
import { AppContextProvider } from "context/AppContextProvider";
import { useOnScreen } from "hooks/useOnScreen";
import { IUtilsMocks } from "types/mocks";
import { ITrackArtist } from "types/spotify";
import { Locale } from "utils";

const { getAllTranslations } = jest.requireActual<IUtilsMocks>(
"utils/__tests__/__mocks__/mocks.ts"
);
jest.mock<typeof import("hooks/useOnScreen")>("hooks/useOnScreen", () => ({
useOnScreen: jest.fn().mockReturnValue(true),
}));
jest.mock("hooks/useLyricsInPictureInPicture");

function setup(props: IArtistListProps) {
const translations = getAllTranslations(Locale.EN);
const view = render(
<AppContextProvider translations={translations}>
<ArtistList {...props} />
</AppContextProvider>
);
return { ...props, ...view };
}

describe("artistList", () => {
const artists: ITrackArtist[] = [
Expand All @@ -19,10 +36,6 @@ describe("artistList", () => {
{ type: "artist" },
];

function setup(props: IArtistListProps) {
return render(<ArtistList {...props} />);
}

it("renders the artist names", () => {
expect.assertions(1);
setup({ artists, maxArtistsToShow: 3 });
Expand Down
13 changes: 8 additions & 5 deletions components/SetList/SetList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,14 @@ export default function SetList({
{set.venue?.name}
</Heading>
<span>
{conjuction([
set.venue?.city.name,
set.venue?.city.state,
set.venue?.city.country.code,
])}
{conjuction(
[
set.venue?.city.name,
set.venue?.city.state,
set.venue?.city.country.code,
],
locale
)}
</span>
</div>
</Link>
Expand Down
1 change: 0 additions & 1 deletion components/SubTitle/SubTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export default function SubTitle({
{year && <>{year} · </>}
{albumType && <>{capitalizeFirstLetter(albumType)} · </>}
<ArtistList artists={artists} maxArtistsToShow={2} />
{artists.length > 2 ? " y más..." : null}
<style jsx>
{`
span :global(a) {
Expand Down
2 changes: 2 additions & 0 deletions context/TranslationsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const TranslationsContext = createContext<
TranslationsContextProviderValue | undefined
>(undefined);

TranslationsContext.displayName = "TranslationsContext";

export interface TranslationsContextProviderProps {
translations: ITranslations;
}
Expand Down
23 changes: 0 additions & 23 deletions utils/__tests__/conjuction.spec.ts

This file was deleted.

86 changes: 86 additions & 0 deletions utils/__tests__/conjuction.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { conjuction, Locale } from "utils";

describe("conjuction", () => {
it("should return a string", () => {
expect.assertions(1);
expect(conjuction([], Locale.EN)).toBe("");
});

it("should return a string a s", () => {
expect.assertions(1);
expect(conjuction(["a"], Locale.EN)).toBe("a");
});

it("should return a string b", () => {
expect.assertions(1);
expect(conjuction(["a", "b"], Locale.EN)).toBe("a, b");
});

it("should return a string c", () => {
expect.assertions(1);
expect(conjuction(["a", "b", "c"], Locale.EN)).toBe("a, b, c");
});

it("should return valid conjunction for spanish", () => {
expect.assertions(1);
expect(conjuction(["a", "b", "c"], Locale.ES)).toBe("a, b y c");
});

it("should return valid conjunction for french", () => {
expect.assertions(1);
expect(conjuction(["a", "b", "c"], "fr")).toBe("a, b et c");
});

it("should return valid nodeList conjunction for spanish and react component", () => {
expect.assertions(1);
expect(
conjuction(
[
<a key={"1"} href="1">
1
</a>,
"b",
"c",
],
Locale.ES
)
).toStrictEqual([
<a key={"1"} href="1">
1
</a>,
", ",
"b",
" y ",
"c",
]);
});

it("should return valid nodeList disjunction for spanish and react component", () => {
expect.assertions(1);
expect(
conjuction(
[
<a key={"1"} href="1">
a
</a>,
<a key={"2"} href="2">
b
</a>,
"c",
],
Locale.ES,
{ type: "disjunction" }
)
).toStrictEqual([
<a key={"1"} href="1">
a
</a>,
", ",
<a key={"2"} href="2">
b
</a>,
" o ",
"c",
]);
});
});
46 changes: 39 additions & 7 deletions utils/conjuction.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,41 @@
export function conjuction(items: (string | undefined)[]): string {
const list: string[] = [];
items.forEach((item) => {
if (item) {
list.push(item);
import { ReactNode } from "react";

export function conjuction(
value: ReactNode[] | string,
locale: string,
options: Intl.ListFormatOptions = { type: "unit" }
): ReactNode[] | string {
const serializedValue: Array<string> = [];
const reactNodes = new Map<string, ReactNode>();

let index = 0;
for (const item of value) {
let serializedItem;
if (typeof item === "object") {
serializedItem = String(index);
reactNodes.set(serializedItem, item);
} else {
serializedItem = String(item);
}
});
return new Intl.ListFormat("en-US", { type: "unit" }).format(list);
serializedValue.push(serializedItem);
index++;
}

try {
const listFormat = new Intl.ListFormat(locale, options);
const formattedParts = listFormat.formatToParts(serializedValue);
const result = formattedParts.map((part) =>
part.type === "literal"
? part.value
: reactNodes.get(part.value) ?? part.value
);

if (reactNodes.size > 0) {
return result;
} else {
return result.join("");
}
} catch (error) {
return String(value);
}
}

0 comments on commit 6a43347

Please sign in to comment.