diff --git a/contentlayer.config.ts b/contentlayer.config.ts index 1b927a8..0bbbdea 100644 --- a/contentlayer.config.ts +++ b/contentlayer.config.ts @@ -1,6 +1,6 @@ import path from "path"; import * as fs from "fs"; -import { createSlug, createText, SpeakerData, TopicsData, unsluggify } from "./src/utils"; +import { createSlug, 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"; @@ -244,37 +244,47 @@ const createTypesCount = (transcripts: ContentTranscriptType[], sources: Content fs.writeFileSync("./public/types-data.json", JSON.stringify(nestedTypes)); }; -function organizeContent(transcripts: ContentTranscriptType[]) { - const tree: ContentTree = {}; +function organizeContent(transcripts: ContentTranscriptType[], sources: ContentSourceType[]) { + const tree: any = {}; - transcripts.forEach((transcript) => { - const parts = transcript.slugAsParams; - let current = tree; - - const isNonEnglishDir = /\w+\.[a-z]{2}\b/.test(parts[parts.length - 1]); - if (isNonEnglishDir) return; + sources.forEach((source) => { + const { _id, slugAsParams, language, _raw, weight, body, hosts, transcription_coverage, url, type, types, ...metaData } = source; + const params = source.slugAsParams; + const topParam = params[0] as string; + const nestedSource = params.length > 1; - for (let i = 0; i < parts.length - 1; i++) { - if (!current[parts[i]]) { - current[parts[i]] = i === parts.length - 2 ? [] : {}; - } - current = current[parts[i]] as ContentTree; + if (!tree[topParam]) { + tree[topParam] = {}; + } + const allTranscriptsForSourceLanguage = transcripts.filter( + (transcript) => transcript._raw.sourceFileDir === source._raw.sourceFileDir && transcript.language === language + ); + + const allTranscriptsForSourceLanguageURLs = allTranscriptsForSourceLanguage.map((transcript) => transcript.url); + + if (!nestedSource) { + tree[topParam] = { + ...tree[topParam], + [language]: { + data: allTranscriptsForSourceLanguageURLs.length ? allTranscriptsForSourceLanguageURLs : {}, + metadata: { + ...metaData, + }, + }, + }; + } else { + tree[topParam][language].data = { + ...tree[topParam][language].data, + [params[1]]: { + data: allTranscriptsForSourceLanguageURLs.length ? allTranscriptsForSourceLanguageURLs : {}, + metadata: { + ...metaData, + }, + }, + }; } - - (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 fs.writeFileSync("./public/sources-data.json", JSON.stringify(tree, null, 2)); } @@ -316,6 +326,14 @@ export const Transcript = defineDocumentType(() => ({ type: "list", resolve: (doc) => doc._raw.flattenedPath.split("/"), }, + language: { + type: "string", + resolve: (doc) => { + const transcript = doc._raw.flattenedPath.split("/").pop(); + const lan = transcript?.split(".").length === 2 ? transcript?.split(".")[1] : "en"; + return lan; + }, + }, }, })); @@ -374,6 +392,6 @@ export default makeSource({ getTranscriptAliases(allTranscripts); createSpeakers(allTranscripts); generateSourcesCount(allTranscripts, allSources); - organizeContent(allTranscripts); + organizeContent(allTranscripts, allSources); }, }); diff --git a/src/app/(explore)/[...slug]/page.tsx b/src/app/(explore)/[...slug]/page.tsx index 90ed7ef..805678c 100644 --- a/src/app/(explore)/[...slug]/page.tsx +++ b/src/app/(explore)/[...slug]/page.tsx @@ -6,75 +6,86 @@ 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 { ContentTree, filterOutIndexes } from "@/utils"; -import BreadCrumbs from "@/components/common/BreadCrumbs"; +import { constructSlugPaths, fetchTranscriptDetails, loopArrOrObject } from "@/utils"; import { ArrowLinkRight } from "@bitcoin-dev-project/bdp-ui/icons"; -import { allSources as allContentSources } from "contentlayer/generated"; +import { allSources as allContentSources, allTranscripts } from "contentlayer/generated"; import TranscriptDetailsCard from "@/components/common/TranscriptDetailsCard"; +import { SourcesBreadCrumbs } from "@/components/explore/SourcesBreadCrumbs"; // forces 404 for paths not generated from `generateStaticParams` function. export const dynamicParams = false; export function generateStaticParams() { - return allContentSources.map(({ slugAsParams }) => ({ slug: slugAsParams })); + const allSlugs = allContentSources.map(({ language, slugAsParams }) => { + const isEnglish = language === "en"; + if (isEnglish) { + return { + slug: slugAsParams, + }; + } + return { + slug: [language, ...slugAsParams], + }; + }); + + return allSlugs; } const page = ({ params }: { params: { slug: string[] } }) => { const slug = params.slug ?? []; const contentTree = allSources; - let current: any = contentTree; + const { slugPaths } = constructSlugPaths(slug); - for (const part of slug) { + for (const part of slugPaths) { if (typeof current === "object" && !Array.isArray(current) && part in current) { - current = current[part] as ContentTree | ContentTreeArray[]; + current = current[part]; } else { notFound(); } } - const displayCurrent = filterOutIndexes(current); - - const pageDetails = allContentSources.find((source) => { - return source.slugAsParams.join("/") === slug.join("/") && source.language === "en"; - }); - - const isDirectoryList = Array.isArray(current); + const metadata = current.metadata; + const data = loopArrOrObject(current?.data); + const isRoot = Array.isArray(current.data); + const { transcripts } = fetchTranscriptDetails(allTranscripts, data, isRoot); return (
- + <> + +

Back

-

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

- {isDirectoryList && pageDetails?.website ? ( +

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

+ {isRoot && metadata?.website ? (
world icon - {pageDetails.website ?? ""} + {metadata.website ?? ""}
) : null} - {isDirectoryList && pageDetails?.additional_resources ? ( + {isRoot && metadata?.additional_resources ? (
link icon
- {pageDetails.additional_resources.map((resource, index) => ( + {metadata.additional_resources.map((resource: any, index: number) => ( {
- {isDirectoryList ? ( + {isRoot ? (
- {(displayCurrent as ContentTreeArray[]) - .sort((a, b) => new Date(b.date!).getTime() - new Date(a.date!).getTime() || a.title.localeCompare(b.title)) - .map((item, i) => ( - - ))} + {(transcripts as ContentTreeArray[]).map((item, i) => ( + + ))}
) : (
-
- {(displayCurrent as string[]).map((key, i) => ( +
+ {(data as any[]).map((value, i) => ( - {key} + {value.title} + {value.count} ))}
diff --git a/src/components/common/TranscriptDetailsCard.tsx b/src/components/common/TranscriptDetailsCard.tsx index e11d10a..f6a0ac9 100644 --- a/src/components/common/TranscriptDetailsCard.tsx +++ b/src/components/common/TranscriptDetailsCard.tsx @@ -15,38 +15,19 @@ const TranscriptDetailsCard = ({ data, slug }: { data: ContentTreeArray; slug: s return (
-
-
-
- {slug - .join(" / ") - .split(" ") - .map((slg, i) => ( -

- {unsluggify(slg)} -

- ))} -
- - {date && ( -
- date icon -

{formatDate(date!)}

-
- )} -
- +
{title} + {date && ( +
+ date icon +

{formatDate(date!)}

+
+ )}
diff --git a/src/components/explore/SourcesBreadCrumbs.tsx b/src/components/explore/SourcesBreadCrumbs.tsx new file mode 100644 index 0000000..f33cd5c --- /dev/null +++ b/src/components/explore/SourcesBreadCrumbs.tsx @@ -0,0 +1,84 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { ExploreNavigationItems } from "@/utils/data"; + +export const SourcesBreadCrumbs = ({ slugPaths, current }: { slugPaths: string[]; current: any }) => { + const pathname = usePathname(); + + const navListWithoutSources = ExploreNavigationItems.filter((item) => item.href !== "/sources").map((item) => item.href.slice(1)); + + const language = slugPaths[1]; + const pathnameArray = pathname.replace(`/${language}`, "").split("/"); + const isNotSourcesPage = navListWithoutSources.includes(pathnameArray[1]); + + const allRoutes = pathnameArray.map((path, idx) => { + const route = pathname + .split("/") + .slice(0, idx + 1) + .join("/"); + return { name: path || "home", link: route || "/" }; + }); + + if (!isNotSourcesPage && pathnameArray[1] !== "sources") { + allRoutes.splice(1, 0, { name: "Sources", link: "/sources" }); + } + + const breadCrumbData = () => { + const _trimPaths = pathnameArray.shift(); + let currentPathArray = pathnameArray; + + const extractedRoutes: Array<{ [key: string]: string }> = []; + const language = slugPaths[1]; + + for (let i = 0; i < pathnameArray.length; i++) { + const pathChoice = i === 0 ? [slugPaths[0], language] : slugPaths; + + for (const part of pathChoice) { + if (typeof current === "object" && !Array.isArray(current) && part in current) { + current = current[part]; + } + } + + extractedRoutes.push({ name: current?.metadata?.title as string, link: currentPathArray[i] }); + } + + return { extractedRoutes }; + }; + + const { extractedRoutes } = breadCrumbData(); + let newRoutes: Array<{ [key: string]: string }> = []; + + if (extractedRoutes.length > 0) { + newRoutes = allRoutes.map((route: any) => { + const isPresent = extractedRoutes.find((extractedRoute) => extractedRoute.link === route.name); + + if (isPresent) { + return { ...route, name: isPresent.name ?? route.name }; + } + return route; + }); + } + + const breadCrumbRoutes = extractedRoutes.length ? newRoutes : allRoutes; + const isActive = breadCrumbRoutes[breadCrumbRoutes.length - 1]; + + return ( +
+ {breadCrumbRoutes.map((link, i) => ( +
+ + {link.name} + + {i !== allRoutes.length - 1 &&

/

} +
+ ))} +
+ ); +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 1bbb208..fde3a46 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -36,30 +36,6 @@ export type DepreciatedCategories = "tags" | "speakers" | "categories" | "source export type GroupedData = Record; -export function organizeContent(transcripts: Transcript[]): ContentTree { - const tree: ContentTree = {}; - - transcripts.forEach((transcript) => { - const parts = transcript.slugAsParams; - let current = tree; - - for (let i = 0; i < parts.length - 1; i++) { - if (!(parts[i] in current)) { - current[parts[i]] = {}; - } - current = current[parts[i]] as ContentTree; - } - - const lastName = parts[parts.length - 1]; - if (!(lastName in current)) { - current[lastName] = []; - } - (current[lastName] as Transcript[]).push(transcript); - }); - - return tree; -} - export function shuffle(data: Transcript[]) { let currIndex = data.length; @@ -196,18 +172,17 @@ export const formatDate = (dateString: string): string | null => { return new Intl.DateTimeFormat("en-GB", options).format(date); }; -export function filterOutIndexes(arr: {} | ContentTreeArray[]) { - let filterIndex: ContentTreeArray[] | string[] = []; +export function loopArrOrObject(arr: {} | ContentTreeArray[]) { + let filterIndex: any[] = []; if (Array.isArray(arr)) { - const list = arr.filter((item: ContentTreeArray) => { - const url = item.flattenedPath.split("/"); - - return !url[url.length - 1].startsWith("_index"); - }); - filterIndex = showOnlyEnglish(list) as ContentTreeArray[]; + filterIndex = arr; } else { - filterIndex = Object.keys(arr).filter((item) => !item.startsWith("_index")); + filterIndex = Object.entries(arr).map(([key, values]) => ({ + route: key, + title: (values as unknown as any).metadata.title, + count: (values as unknown as any).data.length, + })); } return filterIndex; @@ -263,3 +238,48 @@ export const countItemsAndSort = (args: { [category: string]: TagInfo[] }) => { }, {} as typeof countObject); return sortObject; }; + +export const constructSlugPaths = (slug: string[]) => { + const languageCodes = ["zh", "es", "pt"]; + const isEnglishSlug = slug[0] !== "en" && slug[0].length > 2 && !languageCodes.includes(slug[0]); + const englishSlug = ["en", ...slug]; + const newSlug = isEnglishSlug ? [...englishSlug] : [...slug]; + [newSlug[0], newSlug[1]] = [newSlug[1], newSlug[0]]; + + let slugPaths = newSlug; + const addDataKeyToSlug = [...slugPaths.slice(0, 2), "data", ...slugPaths.slice(2)]; + slugPaths = slugPaths.length >= 3 ? addDataKeyToSlug : slugPaths; + + return { slugPaths }; +}; + +export const fetchTranscriptDetails = (allTranscripts: Transcript[], paths: string[], isRoot: boolean) => { + if (!isRoot || paths.length === 0) return { transcripts: [] }; + + const transcripts = allTranscripts.reduce((acc, curr) => { + const { url, title, speakers, date, tags, _raw, summary, body } = curr; + + if (paths.includes(url)) { + acc.push({ + title, + speakers, + date, + tags, + sourceFilePath: _raw.sourceFilePath, + flattenedPath: _raw.flattenedPath, + summary, + body: createText(body), + }); + } + return acc.sort((a, b) => { + const sortByTime = new Date(b.date!).getTime() - new Date(a.date!).getTime(); + const sortByTitle = a.title.localeCompare(b.title); + + return sortByTime || sortByTitle; + }); + }, [] as Array); + + return { + transcripts, + }; +};