From a7b697420e790532b082892b16319ae43f98a2ff Mon Sep 17 00:00:00 2001 From: Solomon eze <87058633+IgboPharaoh@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:09:23 +0100 Subject: [PATCH] Feat/update contentlayer algo (#51) * build: initialize submodules * add source directory * modify types algorithm, contentTree algorithm * move sources to base route * fix: correct back buttton * fix: text corrections, explore navigation state * chore: add additional sources * fix: capitalize types title * sort transcript page * chore: fix count and sort for types and categories * chore: route types to types page --- .gitmodules | 6 +- contentlayer.config.ts | 213 +++++++++--------- next.config.mjs | 12 +- public/refine-taxonomies | 1 + public/svgs/link-icon.svg | 4 + .../{sources => }/[...slug]/page.tsx | 82 ++++--- src/app/(explore)/types/page.tsx | 19 ++ src/components/common/BreadCrumbs.tsx | 12 +- .../common/TranscriptDetailsCard.tsx | 10 +- src/components/explore/ExploreNavigation.tsx | 19 +- .../explore/GroupedTranscriptContent.tsx | 4 +- .../explore/SingleTranscriptContent.tsx | 2 +- .../landing-page/TranscriptCard.tsx | 2 +- .../ExploreTranscriptClient.tsx | 16 +- .../ExploreTranscripts.tsx | 2 +- src/utils/data.ts | 8 +- src/utils/index.ts | 39 +++- 17 files changed, 274 insertions(+), 177 deletions(-) create mode 160000 public/refine-taxonomies create mode 100644 public/svgs/link-icon.svg rename src/app/(explore)/{sources => }/[...slug]/page.tsx (58%) create mode 100644 src/app/(explore)/types/page.tsx diff --git a/.gitmodules b/.gitmodules index 3c9eb36..90a9f3a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,4 +4,8 @@ [submodule "public/gh-pages"] path = public/gh-pages url = https://github.com/bitcointranscripts/bitcointranscripts.github.io.git - branch = gh-pages \ No newline at end of file + branch = gh-pages +[submodule "public/refine-taxonomies"] + path = public/refine-taxonomies + url = https://github.com/kouloumos/bitcointranscripts + branch = refine-taxonomies diff --git a/contentlayer.config.ts b/contentlayer.config.ts index f020bb0..1e49c8b 100644 --- a/contentlayer.config.ts +++ b/contentlayer.config.ts @@ -1,9 +1,8 @@ -import { createSlug, SpeakerData, TopicsData, unsluggify } from "./src/utils"; -import { defineDocumentType, defineNestedType, makeSource } from "contentlayer2/source-files"; -import { writeFileSync } from "fs"; import path from "path"; import * as fs from "fs"; -import { Transcript as ContentTranscriptType, Markdown } from "./.contentlayer/generated/types"; +import { createSlug, createText, SpeakerData, TopicsData, unsluggify } from "./src/utils"; +import { defineDocumentType, defineNestedType, makeSource } from "contentlayer2/source-files"; +import { Transcript as ContentTranscriptType, Source as ContentSourceType } from "./.contentlayer/generated/types"; const Resources = defineNestedType(() => ({ name: "Resources", @@ -64,7 +63,7 @@ const getTranscriptAliases = (allTranscripts: ContentTranscriptType[]) => { } } - writeFileSync("./public/aliases.json", JSON.stringify(aliases)); + fs.writeFileSync("./public/aliases.json", JSON.stringify(aliases)); }; const getCategories = () => { @@ -129,7 +128,7 @@ function organizeTags(transcripts: ContentTranscriptType[]) { tagsByCategory[category].sort((a, b) => a.name.localeCompare(b.name)); }); - writeFileSync("./public/tag-data.json", JSON.stringify(tagsByCategory)); + fs.writeFileSync("./public/tag-data.json", JSON.stringify(tagsByCategory)); return { tagsByCategory, tagsWithoutCategory }; } @@ -159,44 +158,8 @@ function organizeTopics(transcripts: ContentTranscriptType[]) { }); }); - writeFileSync("./public/topics-data.json", JSON.stringify(topicsArray)); + fs.writeFileSync("./public/topics-data.json", JSON.stringify(topicsArray)); } -/** - * Count the occurrences of all types across transcripts and write to json file - */ -const createTypesCount = (allTranscripts: ContentTranscriptType[]) => { - const typesAndCount: Record = {}; - const relevantTypes = [ - "video", - "core-dev-tech", - "podcast", - "conference", - "meeting", - "club", - "meetup", - "hackathon", - "workshop", - "residency", - "developer-tools", - ]; - - allTranscripts.forEach((transcript) => { - if (transcript.categories) { - transcript.categories.forEach((type: string) => { - const formattedType = createSlug(type); - if (relevantTypes.includes(formattedType)) { - if (formattedType in typesAndCount) { - typesAndCount[formattedType] += 1; - } else { - typesAndCount[formattedType] = 1; - } - } - }); - } - }); - - writeFileSync("./public/types-data.json", JSON.stringify(typesAndCount)); -}; function createSpeakers(transcripts: ContentTranscriptType[]) { const slugSpeakers: any = {}; @@ -224,35 +187,63 @@ function createSpeakers(transcripts: ContentTranscriptType[]) { }); }); - writeFileSync("./public/speaker-data.json", JSON.stringify(speakerArray)); + fs.writeFileSync("./public/speaker-data.json", JSON.stringify(speakerArray)); } -function generateSourcesCount(transcripts: ContentTranscriptType[]) { +function generateSourcesCount(transcripts: ContentTranscriptType[], sources: ContentSourceType[]) { const sourcesArray: TagInfo[] = []; const slugSources: Record = {}; transcripts.forEach((transcript) => { const slug = transcript._raw.flattenedPath.split("/")[0]; - const isValid = !!transcript.date; - if (isValid) { - if (slugSources[slug] !== undefined) { - sourcesArray[slugSources[slug]].count += 1; - } else { - const sourcesLength = sourcesArray.length; - slugSources[slug] = sourcesLength; - sourcesArray[sourcesLength] = { - slug, - name: unsluggify(slug), - count: 1, - }; - } + if (slugSources[slug] !== undefined) { + sourcesArray[slugSources[slug]].count += 1; + } else { + const sourcesLength = sourcesArray.length; + slugSources[slug] = sourcesLength; + + const getSourceName = (slug: string) => + sources.find((source) => source.language === "en" && source.slugAsParams[0] === slug)?.title ?? unsluggify(slug); + + sourcesArray[sourcesLength] = { + slug, + name: getSourceName(slug), + count: 1, + }; } }); - writeFileSync("./public/source-count-data.json", JSON.stringify(sourcesArray)); + fs.writeFileSync("./public/source-count-data.json", JSON.stringify(sourcesArray)); + return { sourcesArray, slugSources }; } +const createTypesCount = (transcripts: ContentTranscriptType[], sources: ContentSourceType[]) => { + const { sourcesArray, slugSources } = generateSourcesCount(transcripts, sources); + const nestedTypes: any = {}; + + sources.forEach((transcript) => { + if (transcript.types) { + transcript.types.forEach((type) => { + const slugType = type.charAt(0).toUpperCase() + type.slice(1); + const slug = transcript.slugAsParams[0]; + + const sourceIndex = slugSources[slug]; + const getSource = sourcesArray[sourceIndex] ?? null; + + if (!nestedTypes[slugType]) { + nestedTypes[slugType] = []; + } else { + if (nestedTypes[slugType].includes(getSource) || getSource === null) return; + nestedTypes[slugType].push(getSource); + } + }); + } + }); + + fs.writeFileSync("./public/types-data.json", JSON.stringify(nestedTypes)); +}; + function organizeContent(transcripts: ContentTranscriptType[]) { const tree: ContentTree = {}; @@ -261,46 +252,30 @@ function organizeContent(transcripts: ContentTranscriptType[]) { let current = tree; const isNonEnglishDir = /\w+\.[a-z]{2}\b/.test(parts[parts.length - 1]); - if (isNonEnglishDir) { - return; - } - const loopSize = parts.length === 2 ? parts.length - 1 : parts.length - 2; + if (isNonEnglishDir) return; - for (let i = 0; i < loopSize; i++) { + for (let i = 0; i < parts.length - 1; i++) { if (!current[parts[i]]) { - current[parts[i]] = {}; + current[parts[i]] = i === parts.length - 2 ? [] : {}; } - current = current[parts[i]] as ContentTree; - - const penultimateKey = parts[loopSize]; - - if (!Array.isArray(current[penultimateKey])) { - current[penultimateKey] = []; - } - - const createText = (args: Markdown) => { - const text = args.raw.replace(/]+>|https?:\/\/[^\s]+|##+/g, "").trim(); - - return text.length > 300 ? text.slice(0, 300) + "..." : text; - }; - - (current[penultimateKey] as any[]).push({ - title: transcript.title, - speakers: transcript.speakers, - date: transcript.date, - tags: transcript.tags, - sourceFilePath: transcript._raw.sourceFilePath, - flattenedPath: transcript._raw.flattenedPath, - summary: transcript.summary, - body: createText(transcript.body), - source: transcript.source, - }); } + + (current as unknown as any[]).push({ + title: transcript.title, + speakers: transcript.speakers, + date: transcript.date, + tags: transcript.tags, + sourceFilePath: transcript._raw.sourceFilePath, + flattenedPath: transcript._raw.flattenedPath, + summary: transcript.summary, + body: createText(transcript.body), + source: transcript.source, + }); }); // Save the result as JSON - writeFileSync("./public/sources-data.json", JSON.stringify(tree, null, 2)); + fs.writeFileSync("./public/sources-data.json", JSON.stringify(tree, null, 2)); } export const Transcript = defineDocumentType(() => ({ @@ -329,6 +304,8 @@ export const Transcript = defineDocumentType(() => ({ aditional_resources: { type: "list", of: Resources }, additional_resources: { type: "list", of: Resources }, weight: { type: "number" }, + types: { type: "list", of: { type: "string" } }, + source_file: { type: "string" }, }, computedFields: { url: { @@ -342,9 +319,43 @@ export const Transcript = defineDocumentType(() => ({ }, })); +export const Source = defineDocumentType(() => ({ + name: "Source", + filePathPattern: `**/_index{,.??}.md`, + contentType: "markdown", + fields: { + title: { type: "string", required: true }, + source: { type: "string" }, + transcription_coverage: { type: "string" }, + hosts: { type: "list", of: { type: "string" } }, + weight: { type: "number" }, + website: { type: "string" }, + types: { type: "list", of: { type: "string" } }, + additional_resources: { type: "list", of: Resources }, + }, + computedFields: { + url: { + type: "string", + resolve: (doc) => `/${doc._raw.flattenedPath.split("/").slice(0, -1).join("/")}`, + }, + language: { + type: "string", + resolve: (doc) => { + const index = doc._raw.flattenedPath.split("/").pop(); + const lan = index?.split(".").length === 2 ? index?.split(".")[1] : "en"; + return lan; + }, + }, + slugAsParams: { + type: "list", + resolve: (doc) => doc._raw.flattenedPath.split("/").slice(0, -1), + }, + }, +})); + export default makeSource({ - contentDirPath: path.join(process.cwd(), "public", "bitcoin-transcript"), - documentTypes: [Transcript], + contentDirPath: path.join(process.cwd(), "public", "refine-taxonomies"), + documentTypes: [Source, Transcript], contentDirExclude: [ ".github", ".gitignore", @@ -356,13 +367,13 @@ export default makeSource({ "2018-08-17-richard-bondi-bitcoin-cli-regtest.es.md", ], onSuccess: async (importData) => { - const { allDocuments } = await importData(); - organizeTags(allDocuments); - createTypesCount(allDocuments); - organizeTopics(allDocuments); - getTranscriptAliases(allDocuments); - createSpeakers(allDocuments); - generateSourcesCount(allDocuments); - organizeContent(allDocuments); + const { allTranscripts, allSources } = await importData(); + organizeTags(allTranscripts); + createTypesCount(allTranscripts, allSources); + organizeTopics(allTranscripts); + getTranscriptAliases(allTranscripts); + createSpeakers(allTranscripts); + generateSourcesCount(allTranscripts, allSources); + organizeContent(allTranscripts); }, }); diff --git a/next.config.mjs b/next.config.mjs index 6f15486..f3b54d8 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -5,8 +5,8 @@ const nextConfig = { return { fallback: [ { - source: '/:path*.:ext([^/]+)', // intercept all paths ending with a file extension - destination: '/gh-pages/:path*.:ext', // rewrite to gh-pages/[path_here].ext + source: "/:path*.:ext([^/]+)", // intercept all paths ending with a file extension + destination: "/gh-pages/:path*.:ext", // rewrite to gh-pages/[path_here].ext }, { source: "/transcripts", @@ -20,8 +20,12 @@ const nextConfig = { source: "/:path*", destination: "/gh-pages/:path*/index.html", }, - ] - } + { + source: "/sources/:path((?!.*\\.[^/]+).*)", // Matches /source/[any path without a file extension] + destination: "/[...slug]/:path*", // Replace with your catch-all route + }, + ], + }; }, }; diff --git a/public/refine-taxonomies b/public/refine-taxonomies new file mode 160000 index 0000000..f072492 --- /dev/null +++ b/public/refine-taxonomies @@ -0,0 +1 @@ +Subproject commit f072492c361bd83571897c01a6bfb8ccacb671b3 diff --git a/public/svgs/link-icon.svg b/public/svgs/link-icon.svg new file mode 100644 index 0000000..a682520 --- /dev/null +++ b/public/svgs/link-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/app/(explore)/sources/[...slug]/page.tsx b/src/app/(explore)/[...slug]/page.tsx similarity index 58% rename from src/app/(explore)/sources/[...slug]/page.tsx rename to src/app/(explore)/[...slug]/page.tsx index b72e8a9..90ed7ef 100644 --- a/src/app/(explore)/sources/[...slug]/page.tsx +++ b/src/app/(explore)/[...slug]/page.tsx @@ -1,44 +1,22 @@ import React from "react"; import Link from "next/link"; +import Image from "next/image"; import { notFound } from "next/navigation"; import { ContentTreeArray } from "@/utils/data"; +import LinkIcon from "/public/svgs/link-icon.svg"; +import WorldIcon from "/public/svgs/world-icon.svg"; import allSources from "@/public/sources-data.json"; -import { allTranscripts } from "contentlayer/generated"; +import { ContentTree, filterOutIndexes } from "@/utils"; import BreadCrumbs from "@/components/common/BreadCrumbs"; import { ArrowLinkRight } from "@bitcoin-dev-project/bdp-ui/icons"; -import { ContentTree, extractDirectoryData, filterOutIndexes } from "@/utils"; +import { allSources as allContentSources } from "contentlayer/generated"; import TranscriptDetailsCard from "@/components/common/TranscriptDetailsCard"; -import WorldIcon from "/public/svgs/world-icon.svg"; -import Image from "next/image"; // forces 404 for paths not generated from `generateStaticParams` function. export const dynamicParams = false; export function generateStaticParams() { - // The flattenedPath and SlugParams are destuctured from the value of the currentElement - const all = allTranscripts.reduce((acc, transcript) => { - // params and slugAsParams are more or less the same, continued using both from previous code - const params = transcript._raw.flattenedPath.split("/"); - - // transcript.slugAsParams is typed as a list from contentLayer but its manually casted to `string[]` to specify it's exact type - const slugAsParams = transcript.slugAsParams as unknown as string[]; - - const is_non_english_dir_or_transcript = /\w+\.[a-z]{2}\b/.test(params[params.length - 1]); - - const lastRouteIndex = params[params.length - 1].includes("index"); - - if (!lastRouteIndex) { - acc.push({ slug: slugAsParams }); - // converts slug ['adopting-bitcoin', '_index'] to ['adopting-bitcoin'] - // skips non english dir e.g ['adopting-bitcoin', '_index.es'] is skipped - } else if (!is_non_english_dir_or_transcript) { - acc.push({ slug: slugAsParams.slice(0, slugAsParams.length - 1) }); - } - - return acc; - }, [] as { slug: string[] }[]); - - return all; + return allContentSources.map(({ slugAsParams }) => ({ slug: slugAsParams })); } const page = ({ params }: { params: { slug: string[] } }) => { @@ -57,7 +35,9 @@ const page = ({ params }: { params: { slug: string[] } }) => { const displayCurrent = filterOutIndexes(current); - const { directoryData } = extractDirectoryData(current); + const pageDetails = allContentSources.find((source) => { + return source.slugAsParams.join("/") === slug.join("/") && source.language === "en"; + }); const isDirectoryList = Array.isArray(current); @@ -71,34 +51,52 @@ const page = ({ params }: { params: { slug: string[] } }) => { >
- +

Back

-

- {current["_index"] ? current["_index"][0].title : isDirectoryList ? directoryData?.title : slug[slug.length - 1]} -

- {isDirectoryList && directoryData?.source ? ( -
+

{pageDetails?.title ?? slug[slug.length - 1]}

+ {isDirectoryList && pageDetails?.website ? ( +
world icon - {directoryData?.source} + {pageDetails.website ?? ""}
) : null} + + {isDirectoryList && pageDetails?.additional_resources ? ( +
+ link icon +
+ {pageDetails.additional_resources.map((resource, index) => ( + + {resource.title} + + ))} +
+
+ ) : null}
{isDirectoryList ? (
- {(displayCurrent as ContentTreeArray[]).map((item, i) => ( - - ))} + {(displayCurrent as ContentTreeArray[]) + .sort((a, b) => new Date(b.date!).getTime() - new Date(a.date!).getTime() || a.title.localeCompare(b.title)) + .map((item, i) => ( + + ))}
) : (
@@ -106,7 +104,7 @@ const page = ({ params }: { params: { slug: string[] } }) => { {(displayCurrent as string[]).map((key, i) => ( {key} diff --git a/src/app/(explore)/types/page.tsx b/src/app/(explore)/types/page.tsx new file mode 100644 index 0000000..c61cd31 --- /dev/null +++ b/src/app/(explore)/types/page.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import TranscriptContentPage from "@/components/explore/TranscriptContentPage"; +import allTypesData from "@/public/types-data.json"; + +const CategoriesPage = () => { + return ( +
+ +
+ ); +}; + +export default CategoriesPage; diff --git a/src/components/common/BreadCrumbs.tsx b/src/components/common/BreadCrumbs.tsx index 834d6c2..cd0645c 100644 --- a/src/components/common/BreadCrumbs.tsx +++ b/src/components/common/BreadCrumbs.tsx @@ -3,11 +3,17 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import React from "react"; +import { ExploreNavigationItems } from "@/utils/data"; const BreadCrumbs = () => { const pathname = usePathname(); - const allRoutes = pathname.split("/").map((path, idx) => { + const navListWithoutSources = ExploreNavigationItems.filter((item) => item.href !== "/sources").map((item) => item.href.slice(1)); + + const pathnameArray = pathname.split("/"); + const isNotSourcesPage = navListWithoutSources.includes(pathnameArray[1]); + + const allRoutes = pathnameArray.map((path, idx) => { const route = pathname .split("/") .slice(0, idx + 1) @@ -15,6 +21,10 @@ const BreadCrumbs = () => { return { name: path || "home", link: route || "/" }; }); + if (!isNotSourcesPage && pathnameArray[1] !== "sources") { + allRoutes.splice(1, 0, { name: "Sources", link: "/sources" }); + } + const isActive = allRoutes[allRoutes.length - 1]; return ( diff --git a/src/components/common/TranscriptDetailsCard.tsx b/src/components/common/TranscriptDetailsCard.tsx index 1f5b9a2..e11d10a 100644 --- a/src/components/common/TranscriptDetailsCard.tsx +++ b/src/components/common/TranscriptDetailsCard.tsx @@ -33,10 +33,12 @@ const TranscriptDetailsCard = ({ data, slug }: { data: ContentTreeArray; slug: s ))}
-
- date icon -

{formatDate(date!)}

-
+ {date && ( +
+ date icon +

{formatDate(date!)}

+
+ )} { const ExploreNavigationItem = ({ href, title }: { href: string; title: string }) => { const pathname = usePathname(); - const pagePath = pathname.split("/")[1].toLowerCase(); - const isActive = pagePath === title.toLowerCase(); + let pagePath = pathname.split("/")[1].toLowerCase(); + + const switchState = () => { + let isActive = false; + const navList = ExploreNavigationItems.map((item) => item.title.toLowerCase()).includes(pagePath); + + if (navList) { + isActive = pagePath === title.toLowerCase(); + } else if (!navList) { + pagePath = "sources"; + isActive = pagePath === title.toLowerCase(); + } + + return { isActive }; + }; + + const { isActive } = switchState(); return ( -

{topicsByAlphabet[0]}

+

{topicsByAlphabet[0]}

{topicsByAlphabet[1].map((topics, i) => ( -

{topicsByAlphabet[0]}

+

{topicsByAlphabet[0]}

{topicsByAlphabet[1] && topicsByAlphabet[1].map((data, i) => ( diff --git a/src/components/explore/SingleTranscriptContent.tsx b/src/components/explore/SingleTranscriptContent.tsx index 59df0c8..bdcce6f 100644 --- a/src/components/explore/SingleTranscriptContent.tsx +++ b/src/components/explore/SingleTranscriptContent.tsx @@ -6,7 +6,7 @@ type SingleContent = { linkName: DepreciatedCategories; } & TopicsData; const SingleTranscriptContent = ({ count, slug, name, linkName }: SingleContent) => { - const url = `/${linkName}/${slug}`; + const url = linkName === "sources" ? `/${slug}` : `/${linkName}/${slug}`; return ( { - const sortedCategories = Object.fromEntries( - Object.entries(categories) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([key, value]) => [key, value.sort((a, b) => a.name.localeCompare(b.name))]) - ); - - const sortedTypes = Object.fromEntries(Object.entries(types).sort(([a], [b]) => a.localeCompare(b))); + const sortedCategories = countItemsAndSort(categories); + const sortedTypes = countItemsAndSort(types); return (
@@ -43,7 +37,7 @@ const ExploreTranscriptClient = ({ categories, types }: ExploreTranscriptClientP {Object.entries(sortedCategories).map(([key, value]) => ( - + ))} diff --git a/src/components/landing-page/explore-transcripts/ExploreTranscripts.tsx b/src/components/landing-page/explore-transcripts/ExploreTranscripts.tsx index de5bffc..440a328 100644 --- a/src/components/landing-page/explore-transcripts/ExploreTranscripts.tsx +++ b/src/components/landing-page/explore-transcripts/ExploreTranscripts.tsx @@ -10,7 +10,7 @@ function getTags() { return JSON.parse(fileContents); } -const getTypes = (): { [key: string]: number } => { +const getTypes = () => { const filePath = path.join(process.cwd(), "public", "types-data.json"); const fileContents = fs.readFileSync(filePath, "utf8"); return JSON.parse(fileContents); diff --git a/src/utils/data.ts b/src/utils/data.ts index 58a6c34..29f3f32 100644 --- a/src/utils/data.ts +++ b/src/utils/data.ts @@ -66,10 +66,10 @@ export const ExploreNavigationItems = [ href: "/speakers", title: "Speakers", }, - // { - // href: "/types", - // title: "Types", - // }, + { + href: "/types", + title: "Types", + }, { href: "/sources", title: "Sources", diff --git a/src/utils/index.ts b/src/utils/index.ts index a426c0b..1bbb208 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,4 @@ -import { type Transcript } from "contentlayer/generated"; +import { Markdown, type Transcript } from "contentlayer/generated"; import { ContentTreeArray } from "./data"; export interface ContentTree { @@ -22,11 +22,17 @@ export type ContentData = { count: number; }; +interface TagInfo { + name: string; + slug: string; + count: number; +} + type ContentKeys = { [key: string]: ContentData[]; }; -export type DepreciatedCategories = "tags" | "speakers" | "categories" | "sources"; +export type DepreciatedCategories = "tags" | "speakers" | "categories" | "sources" | "types"; export type GroupedData = Record; @@ -228,3 +234,32 @@ export const extractDirectoryData = (data: any[]) => { return { directoryData }; }; + +export const createText = (args: Markdown) => { + const text = args.raw.replace(/]+>|https?:\/\/[^\s]+|##+/g, "").trim(); + return text.length > 300 ? text.slice(0, 300) + "..." : text; +}; + +export const sortObjectAndArrays = (args: { [category: string]: TagInfo[] }) => { + return Object.fromEntries( + Object.entries(args) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, value]) => [key, value.sort((a, b) => a.name.localeCompare(b.name))]) + ); +}; + +export const countItemsAndSort = (args: { [category: string]: TagInfo[] }) => { + const countObject: { [key: string]: number } = {}; + + Object.entries(args).map(([key, value]) => { + countObject[key] = value.reduce((acc, curr) => acc + curr.count, 0); + }); + + const sortObject: { [key: string]: number } = Object.keys(countObject) + .sort() + .reduce((acc, curr) => { + acc[curr] = countObject[curr]; + return acc; + }, {} as typeof countObject); + return sortObject; +};