diff --git a/contentlayer.config.ts b/contentlayer.config.ts index f020bb0..d2bad6f 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, 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,34 +187,59 @@ 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; + if (slugSources[slug] !== undefined) { + sourcesArray[slugSources[slug]].count += 1; + } else { + const sourcesLength = sourcesArray.length; + slugSources[slug] = sourcesLength; + const getTranscriptName = (slug: string) => + sources.find((source) => source.language === "en" && source.slugAsParams[0] === slug)?.title ?? unsluggify(slug); + + sourcesArray[sourcesLength] = { + slug, + name: getTranscriptName(slug), + count: 1, + }; + } + }); + + 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) { + const type = transcript.types[0]; + const slug = transcript.slugAsParams[0]; + + const sourceIndex = slugSources[slug]; + const getSource = sourcesArray[sourceIndex] ?? null; + + if (!nestedTypes[type]) { + nestedTypes[type] = []; } else { - const sourcesLength = sourcesArray.length; - slugSources[slug] = sourcesLength; - sourcesArray[sourcesLength] = { - slug, - name: unsluggify(slug), - count: 1, - }; + if (nestedTypes[type].includes(getSource) || getSource === null) return; + nestedTypes[type].push(getSource); } } }); - writeFileSync("./public/source-count-data.json", JSON.stringify(sourcesArray)); -} + fs.writeFileSync("./public/types-data.json", JSON.stringify(nestedTypes)); +}; function organizeContent(transcripts: ContentTranscriptType[]) { const tree: ContentTree = {}; @@ -261,46 +249,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: transcript.body.raw, + 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,22 +301,73 @@ 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: { type: "string", resolve: (doc) => `/${doc._raw.flattenedPath}`, }, + language: { + type: "string", + resolve: (doc) => { + const transcript = doc._raw.flattenedPath.split("/").pop(); + const lan = transcript?.split(".").length === 2 ? transcript?.split(".")[1] : "en"; + return lan; + }, + }, + slugAsParams: { + type: "list", + resolve: (doc) =>{ + const paths = doc._raw.flattenedPath.split("/") + const isNonEnglishTranscript = paths[paths.length -1].match(/\w+\.[a-z]{2}\b/) + if(isNonEnglishTranscript){ + paths[paths.length - 1] = paths[paths.length -1 ].split(".")[0] + } + return paths + }, + }, + }, +})); + +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("/"), + resolve: (doc) => doc._raw.flattenedPath.split("/").slice(0, -1), }, }, })); export default makeSource({ contentDirPath: path.join(process.cwd(), "public", "bitcoin-transcript"), - documentTypes: [Transcript], + documentTypes: [Source, Transcript], contentDirExclude: [ ".github", ".gitignore", @@ -356,13 +379,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); }, -}); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ff75363..bd82fde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@bitcoin-dev-project/bdp-ui": "^1.3.0", "contentlayer2": "^0.4.6", + "date-fns": "^4.1.0", "next": "14.2.4", "next-contentlayer2": "^0.4.6", "react": "^18", @@ -2080,6 +2081,15 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", diff --git a/package.json b/package.json index b71d273..98017ef 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,13 @@ "dependencies": { "@bitcoin-dev-project/bdp-ui": "^1.3.0", "contentlayer2": "^0.4.6", + "date-fns": "^4.1.0", "next": "14.2.4", "next-contentlayer2": "^0.4.6", "react": "^18", "react-dom": "^18", - "tailwind-merge": "^2.5.2", - "react-intersection-observer": "^9.13.1" + "react-intersection-observer": "^9.13.1", + "tailwind-merge": "^2.5.2" }, "devDependencies": { "@types/node": "^20", diff --git a/src/app/dummy/page.tsx b/src/app/dummy/page.tsx deleted file mode 100644 index cdcc5ff..0000000 --- a/src/app/dummy/page.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from "react"; -import Wrapper from "@/components/layout/Wrapper"; -import BreadCrumbs from "@/components/common/BreadCrumbs"; -import TranscriptMetadataComponent from "@/components/common/TranscriptMetadatCard"; -import ContentSwitch from "@/components/common/ContentSwitch"; - -const page = () => { - return ( -
- -
-
- -
- -
-
-
- -
- -
-
- -
-
-
- -
-
- {/* body */} -

{}

-
-
-
-
-
-
- ); -}; - -export default page; diff --git a/src/app/transcript/[...slug]/page.tsx b/src/app/transcript/[...slug]/page.tsx new file mode 100644 index 0000000..856bc72 --- /dev/null +++ b/src/app/transcript/[...slug]/page.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { allTranscripts } from "contentlayer/generated"; +import { LanguageCodes } from "@/config"; +import { notFound } from "next/navigation"; +import IndividualTranscript from "@/components/individual-transcript/IndividualTranscript"; + +export function generateStaticParams() { + + const allSingleTranscriptPaths = allTranscripts.map((transcript) => { + const isLanguageEnglish = transcript.language === "en"; + if (isLanguageEnglish) { + return { + slug: transcript.slugAsParams as string[], + }; + } + return { + slug: [transcript.language, ...transcript.slugAsParams] as string[], + }; + }); + + return allSingleTranscriptPaths; +} + +const Page = ({ params }: { params: { slug: string[] } }) => { + const slugArray = params.slug; + const isNonEnglishLanguage = LanguageCodes.includes(slugArray[0]); + let transcriptUrl = ""; + if (isNonEnglishLanguage) { + const languageCode = slugArray.shift(); + slugArray[slugArray.length - 1] = slugArray[slugArray.length - 1] + `.${languageCode}`; + transcriptUrl = `/${slugArray.join("/")}`; + } else { + transcriptUrl = `/${slugArray.join("/")}`; + } + + const transcript = allTranscripts.find(transcript => transcript.url === transcriptUrl) + + if(!transcript) { + return notFound(); + } + return ( +
+ +
+ ); +}; + +export default Page; diff --git a/src/components/common/ContentSwitch.tsx b/src/components/common/ContentSwitch.tsx index 30d6582..a37fcbc 100644 --- a/src/components/common/ContentSwitch.tsx +++ b/src/components/common/ContentSwitch.tsx @@ -1,32 +1,72 @@ "use client"; +import { Resources } from "contentlayer/generated"; import React, { useState } from "react"; +import TranscriptTabContent from "../individual-transcript/TranscriptTabContent"; -const ContentSwitch = () => { - const [openSection, setOpenSection] = useState({ transcript: true, summary: false, extraInfo: false }); +const ContentSwitch = ({ + summary, + markdown, + extraInfo, +}: { + summary?: string; + markdown: string; + extraInfo?: Resources[]; +}) => { + const [openSection, setOpenSection] = useState({ + transcript: true, + summary: false, + extraInfo: false, + }); return ( -
-
+
+
setOpenSection((prev) => ({ ...prev, transcript: true, summary: false, extraInfo: false }))} - /> - setOpenSection((prev) => ({ ...prev, summary: true, transcript: false, extraInfo: false }))} - /> - setOpenSection((prev) => ({ ...prev, extraInfo: true, transcript: false, summary: false }))} + onClick={() => + setOpenSection((prev) => ({ + ...prev, + transcript: true, + summary: false, + extraInfo: false, + })) + } /> + {summary && ( + + setOpenSection((prev) => ({ + ...prev, + summary: true, + transcript: false, + extraInfo: false, + })) + } + /> + )} + + {extraInfo && ( + + setOpenSection((prev) => ({ + ...prev, + extraInfo: true, + transcript: false, + summary: false, + })) + } + /> + )}
-
- {openSection.transcript ?
transcript
: null} +
+ {openSection.transcript ? : null} {openSection.summary ?
summary
: null} {openSection.extraInfo ?
extra info
: null}
@@ -34,12 +74,27 @@ const ContentSwitch = () => { ); }; -const SwitchItem = ({ title, isOpen, onClick }: { title: string; isOpen: boolean; onClick: () => void }) => { +const SwitchItem = ({ + title, + isOpen, + onClick, +}: { + title: string; + isOpen: boolean; + onClick: () => void; +}) => { return ( - ); diff --git a/src/components/common/TranscriptMetadatCard.tsx b/src/components/common/TranscriptMetadatCard.tsx deleted file mode 100644 index 1412aa9..0000000 --- a/src/components/common/TranscriptMetadatCard.tsx +++ /dev/null @@ -1,136 +0,0 @@ -"use client"; - -import React, { useState } from "react"; -import Image from "next/image"; -import { BookmarkIcon, CalendarIcon, MicIcon } from "@bitcoin-dev-project/bdp-ui/icons"; -import Link from "next/link"; -import { createSlug } from "@/utils"; - -interface ITranscriptMetadataComponent { - title: string; - date: string | Date; - topics: string[]; - speakers: string[] | null; - transcriptBy: string | string[]; -} - -const TranscriptMetadataComponent = ({ title, speakers, topics, transcriptBy }: ITranscriptMetadataComponent) => { - const [showDetail, setShowDetail] = useState(true); - - const handleShowDetail = () => { - setShowDetail((prev) => !prev); - }; - - return ( -
-
-

{title}

- -
- {/* Depends on the Show and Hide Button */} - {showDetail && ( -
- - -

Date

- - } - footer={ -
-

7 March, 2024

-
- } - /> - {/* render only 3 tags*/} - - -

Topic

- - } - footer={ -
- {topics && - topics.map((topic) => ( - - {topic} - - ))} -
- } - /> - - - -

Speakers

- - } - footer={ -
- {speakers && - speakers.map((speaker) => ( - - {speaker} - - ))} -
- } - /> - - - pencil icon -

Transcript by

- - } - footer={ -
-

{transcriptBy}

-
- } - /> -
- )} -
- ); -}; - -const MetadataBlock = ({ header, footer }: { header: React.ReactNode; footer: React.ReactNode }) => { - return ( -
-
{header}
-
{footer}
-
- ); -}; - -export default TranscriptMetadataComponent; diff --git a/src/components/common/TranscriptMetadataCard.tsx b/src/components/common/TranscriptMetadataCard.tsx new file mode 100644 index 0000000..0006ce1 --- /dev/null +++ b/src/components/common/TranscriptMetadataCard.tsx @@ -0,0 +1,198 @@ +"use client"; + +import React, { useState } from "react"; +import Image from "next/image"; +import { + BookmarkIcon, + CalendarIcon, + MicIcon, +} from "@bitcoin-dev-project/bdp-ui/icons"; +import Link from "next/link"; +import { createSlug } from "@/utils"; +import AiGeneratedIcon from "../svgs/AIGeneratedIcon"; +import { format, isDate } from "date-fns"; + +interface ITranscriptMetadataComponent { + title: string; + date: string | Date; + topics: string[]; + speakers: string[] | null; + transcriptBy: string | string[]; +} + +const TranscriptMetadataComponent = ({ + title, + speakers, + topics, + date, + transcriptBy, +}: ITranscriptMetadataComponent) => { + const [showDetail, setShowDetail] = useState(true); + const isAiGenerated = transcriptBy.includes("needs-review") ? true : false; + const handleShowDetail = () => { + setShowDetail((prev) => !prev); + }; + + const convertedDate = new Date(date); + + const formattedDate = isDate(convertedDate) ? format(convertedDate, "d MMMM, yyyy") : ""; + + return ( +
+
+

+ {title} +

+ +
+ {/* Depends on the Show and Hide Button */} + {showDetail && ( +
+ + +

Date

+ + } + footer={ +
+

+ {formattedDate} +

+
+ } + /> + {/* render only 3 tags*/} + + +

Topic

+ + } + footer={ +
+ {topics && + topics.map((topic) => ( + + {topic} + + ))} +
+ } + /> + + + +

Speakers

+ + } + footer={ +
+ {speakers && + speakers.map((speaker) => ( + + {speaker} + + ))} +
+ } + /> + + + pencil icon +

+ Transcript by +

+ + } + footer={ +
+ {isAiGenerated ? ( + <> + + + + AI Generated (Review for sats) + + + {" "} + + ) : ( +

+ {transcriptBy} {" via review.btctranscripts.com"} +

+ )} +
+ } + /> +
+ )} +
+ ); +}; + +const MetadataBlock = ({ + header, + footer, +}: { + header: React.ReactNode; + footer: React.ReactNode; +}) => { + return ( +
+
{header}
+
{footer}
+
+ ); +}; + +export default TranscriptMetadataComponent; diff --git a/src/components/individual-transcript/IndividualTranscript.tsx b/src/components/individual-transcript/IndividualTranscript.tsx new file mode 100644 index 0000000..37c4f84 --- /dev/null +++ b/src/components/individual-transcript/IndividualTranscript.tsx @@ -0,0 +1,66 @@ +"use client"; +import React, { useState } from "react"; +import { Transcript } from "contentlayer/generated"; +import BreadCrumbs from "../common/BreadCrumbs"; +import TranscriptMetadataComponent from "../common/TranscriptMetadataCard"; +import ContentSwitch from "../common/ContentSwitch"; +import AlphabetGrouping from "../explore/AlphabetGrouping"; +import ContentGrouping from "../explore/ContentGrouping"; +import { createSlug, extractListOfHeadings } from "@/utils"; + +const IndividualTranscript = ({ transcript }: { transcript: Transcript }) => { + const [currentHeading, setCurrentHeading] = useState(""); + const allHeadings = extractListOfHeadings(transcript.body.raw).map( + (heading) => ({ + name: heading.replace(/[#]+\s+/gi, ""), + slug: createSlug(heading.replace(/[#]+\s+/gi, "")), + count: 0, + }) + ); + const groupedHeading = allHeadings.reduce( + (heading, headingNow) => ({ ...heading, [headingNow.name]: headingNow }), + {} + ); + + + return ( +
+ + +
+
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+ ); +}; + +export default IndividualTranscript; diff --git a/src/components/individual-transcript/TranscriptTabContent.tsx b/src/components/individual-transcript/TranscriptTabContent.tsx new file mode 100644 index 0000000..2acb76b --- /dev/null +++ b/src/components/individual-transcript/TranscriptTabContent.tsx @@ -0,0 +1,9 @@ +import { extractListOfHeadings } from "@/utils"; +import React from "react"; + +const TranscriptTabContent = ({ markdown }: { markdown: string }) => { + + return
{markdown}
; +}; + +export default TranscriptTabContent; diff --git a/src/components/svgs/AIGeneratedIcon.tsx b/src/components/svgs/AIGeneratedIcon.tsx new file mode 100644 index 0000000..902b336 --- /dev/null +++ b/src/components/svgs/AIGeneratedIcon.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { SVGProps } from "react"; +const AiGeneratedIcon = (props: SVGProps) => ( + + + + + + + + + + +); +export default AiGeneratedIcon; diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..6597631 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1 @@ +export const LanguageCodes = ["zh", "es", "pt"] \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index a426c0b..fc201a9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,6 @@ import { type Transcript } from "contentlayer/generated"; import { ContentTreeArray } from "./data"; +import { LanguageCodes } from "@/config"; export interface ContentTree { [key: string]: ContentTree | Transcript[]; @@ -208,8 +209,8 @@ export function filterOutIndexes(arr: {} | ContentTreeArray[]) { } export const showOnlyEnglish = (args: ContentTreeArray[]) => { - const languageCodes = ["zh", "es", "pt"]; - const languageRegex = new RegExp(`\\.(${languageCodes.join("|")})(\\.md)?$`); + + const languageRegex = new RegExp(`\\.(${LanguageCodes.join("|")})(\\.md)?$`); const transcripts = args.filter((transcript) => { return !languageRegex.test(transcript.flattenedPath); @@ -228,3 +229,17 @@ export const extractDirectoryData = (data: any[]) => { return { directoryData }; }; + + +export function extractListOfHeadings(text: string): string[] { + const lines: string[] = text.split('\n'); + const headings: string[] = []; + + lines.forEach(line => { + if (line.match(/[#]+\s+\w+/gi)) { + headings.push(line.trim()); + } + }); + + return headings; +} \ No newline at end of file