Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: source data structure to account for languages and metadata #57

Merged
merged 2 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 46 additions & 28 deletions contentlayer.config.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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));
}

Expand Down Expand Up @@ -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;
},
},
},
}));

Expand Down Expand Up @@ -374,6 +392,6 @@ export default makeSource({
getTranscriptAliases(allTranscripts);
createSpeakers(allTranscripts);
generateSourcesCount(allTranscripts, allSources);
organizeContent(allTranscripts);
organizeContent(allTranscripts, allSources);
},
});
76 changes: 43 additions & 33 deletions src/app/(explore)/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className='flex items-start lg:gap-[50px]'>
<div className='flex flex-col w-full gap-6 md:gap-8 2xl:gap-10 no-scrollbar'>
<div
className={`flex flex-col ${
isDirectoryList ? "border-b border-b-[#9B9B9B] pb-6 md:border-b-0 md:pb-0" : "border-b border-b-[#9B9B9B] pb-6 lg:pb-10"
isRoot ? "border-b border-b-[#9B9B9B] pb-6 md:border-b-0 md:pb-0" : "border-b border-b-[#9B9B9B] pb-6 lg:pb-10"
} gap-5 2xl:gap-6`}
>
<BreadCrumbs />
<>
<SourcesBreadCrumbs slugPaths={slugPaths} current={contentTree} />
</>
<div className='flex flex-col'>
<Link href={slug.slice(0, -1).join("/") === "" ? `/sources` : `/${slug.slice(0, -1).join("/")}`} className='flex gap-1 items-center'>
<ArrowLinkRight className='rotate-180 w-5 md:w-6' />
<p>Back</p>
</Link>

<h3 className='text-xl 2xl:text-2xl font-medium pt-6 md:pt-3'>{pageDetails?.title ?? slug[slug.length - 1]}</h3>
{isDirectoryList && pageDetails?.website ? (
<h3 className='text-xl 2xl:text-2xl font-medium pt-6 md:pt-3'>{metadata?.title ?? slug[slug.length - 1]}</h3>
{isRoot && metadata?.website ? (
<div className='flex gap-1 items-center pt-3 md:pt-6'>
<Image src={WorldIcon} alt='world icon' className='w-[18px] md:w-[20px]' />
<Link
href={pageDetails?.website ?? ""}
href={metadata?.website ?? ""}
target='_blank'
className='text-xs md:text-sm xl:text-base leading-[17.6px] font-medium text-black underline text-wrap break-words line-clamp-1'
>
{pageDetails.website ?? ""}
{metadata.website ?? ""}
</Link>
</div>
) : null}

{isDirectoryList && pageDetails?.additional_resources ? (
{isRoot && metadata?.additional_resources ? (
<div className='flex gap-1 items-center pt-3 md:pt-6'>
<Image src={LinkIcon} alt='link icon' className='w-[18px] md:w-[20px]' />
<div className='flex gap-1 flex-wrap'>
{pageDetails.additional_resources.map((resource, index) => (
{metadata.additional_resources.map((resource: any, index: number) => (
<Link
href={resource.url ?? ""}
key={`${resource.title}-${index}`}
Expand All @@ -90,24 +101,23 @@ const page = ({ params }: { params: { slug: string[] } }) => {
</div>
</div>

{isDirectoryList ? (
{isRoot ? (
<div className='flex flex-col gap-6 h-full pb-8 overflow-scroll'>
{(displayCurrent as ContentTreeArray[])
.sort((a, b) => new Date(b.date!).getTime() - new Date(a.date!).getTime() || a.title.localeCompare(b.title))
.map((item, i) => (
<TranscriptDetailsCard key={i} slug={slug} data={item} />
))}
{(transcripts as ContentTreeArray[]).map((item, i) => (
<TranscriptDetailsCard key={i} slug={slug} data={item} />
))}
</div>
) : (
<div className='flex-col flex gap-10 overflow-scroll pb-8'>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-2.5'>
{(displayCurrent as string[]).map((key, i) => (
<div className='grid grid-cols-1 sm:grid-cols-2 gap-2.5 '>
{(data as any[]).map((value, i) => (
<Link
key={`${key}-${i}}`}
href={`/${[...slug, key].join("/")}`}
key={`${value.route}-${i}}`}
href={`/${[...slug, value.route].join("/")}`}
className='flex capitalize cursor-pointer border max-w-[100%] border-gray-custom-1200 rounded-[5px] justify-between items-center text-sm py-5 px-4 lg:py-7 2xl:px-6 2xl:text-lg font-semibold text-gray-custom-1100'
>
<span className='text-wrap break-words max-w-[80%]'>{key}</span>
<span className='text-wrap break-words max-w-[80%]'>{value.title}</span>
<span>{value.count}</span>
</Link>
))}
</div>
Expand Down
33 changes: 7 additions & 26 deletions src/components/common/TranscriptDetailsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,19 @@ const TranscriptDetailsCard = ({ data, slug }: { data: ContentTreeArray; slug: s
return (
<div className='border border-gray-custom-1200 rounded-lg p-4 md:p-5 2xl:p-6 flex flex-col gap-3 md:gap-4'>
<section className='flex justify-between'>
<div className='flex flex-col gap-2 w-full'>
<section className='flex flex-col md:flex-row gap-3 md:justify-between md:items-center w-full'>
<div className='flex gap-2 flex-wrap'>
{slug
.join(" / ")
.split(" ")
.map((slg, i) => (
<p
key={`${slg}-${i}`}
className={`text-xs md:text-sm 2xl:text-base leading-[20.64px] ${
slg === "/" ? "text-custom-black-custom-200" : "text-gray-custom-800"
} font-medium capitalize`}
>
{unsluggify(slg)}
</p>
))}
</div>

{date && (
<div className='flex gap-2 items-center h-fit'>
<Image src={DateIcon} alt='date icon' className='w-[18px] md:w-[20px]' />
<p className='text-xs md:text-sm 2xl:text-base leading-[17.6px] font-medium text-gray-custom-800'>{formatDate(date!)}</p>
</div>
)}
</section>

<div className='flex md:flex-row flex-col justify-between gap-2 w-full'>
<Link
href={`/${url}`}
className='font-bold text-base leading-[21.86px] md:text-xl 2xl:text-[22.5px] md:leading-[30px] text-orange-custom-100 md:text-black'
>
{title}
</Link>
{date && (
<div className='flex gap-2 items-center h-fit'>
<Image src={DateIcon} alt='date icon' className='w-[18px] md:w-[20px]' />
<p className='text-xs md:text-sm 2xl:text-base leading-[17.6px] font-medium text-gray-custom-800'>{formatDate(date!)}</p>
</div>
)}
</div>
</section>

Expand Down
84 changes: 84 additions & 0 deletions src/components/explore/SourcesBreadCrumbs.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className='flex gap-1 flex-wrap'>
{breadCrumbRoutes.map((link, i) => (
<div key={link.name} className='flex gap-1 items-center'>
<Link
className={`capitalize hover:underline font-medium text-sm 2xl:text-base text-nowrap ${
isActive.name.toLowerCase() === link.name.toLowerCase() ? "text-orange-custom-100" : "text-black md:text-gray-custom-800"
}`}
href={link.link}
>
{link.name}
</Link>
{i !== allRoutes.length - 1 && <p className='text-custom-black-custom-200'>/</p>}
</div>
))}
</div>
);
};
Loading