From f4467c34fe2d5cd90519380e0612efd800ead07f Mon Sep 17 00:00:00 2001 From: Devesh Chandra Date: Sun, 16 Nov 2025 15:47:42 +0530 Subject: [PATCH 01/14] add: added the newsletter listing page, and the newsletter page, markdown exporter function --- apps/web/content/newsletters/test.md | 11 + apps/web/package.json | 6 + .../dashboard/newsletters/NewsletterPage.tsx | 165 ++++ .../dashboard/newsletters/[slug]/page.tsx | 111 +++ .../app/(main)/dashboard/newsletters/page.tsx | 8 + apps/web/src/components/dashboard/Sidebar.tsx | 33 +- .../src/components/newsletter/Dropdown.tsx | 77 ++ .../components/newsletter/NewsletterItem.tsx | 81 ++ .../web/src/utils/newsletter/getNewsLetter.ts | 42 + apps/web/src/utils/newsletter/markdown.ts | 32 + apps/web/tailwind.config.ts | 90 +- pnpm-lock.yaml | 819 ++++++++++++++++++ 12 files changed, 1472 insertions(+), 3 deletions(-) create mode 100644 apps/web/content/newsletters/test.md create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/page.tsx create mode 100644 apps/web/src/components/newsletter/Dropdown.tsx create mode 100644 apps/web/src/components/newsletter/NewsletterItem.tsx create mode 100644 apps/web/src/utils/newsletter/getNewsLetter.ts create mode 100644 apps/web/src/utils/newsletter/markdown.ts diff --git a/apps/web/content/newsletters/test.md b/apps/web/content/newsletters/test.md new file mode 100644 index 00000000..4f8d1e37 --- /dev/null +++ b/apps/web/content/newsletters/test.md @@ -0,0 +1,11 @@ +--- +id: "unique-newsletter-id" +title: "Your Newsletter Title Goes Here" +date: "2025-11-16" +summary: "A brief 1-2 sentence summary of what this newsletter is about. This will appear below the title." +keywords: ["keyword1", "keyword2", "keyword3"] +readTime: "5 min read" +slug: "your-newsletter-slug" +--- + +## Add the content here diff --git a/apps/web/package.json b/apps/web/package.json index 77494fc3..2d75cb0e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -27,7 +27,9 @@ "dompurify": "^3.3.0", "framer-motion": "^11.15.0", "geist": "^1.5.1", + "gray-matter": "^4.0.3", "lucide-react": "^0.456.0", + "marked": "^17.0.0", "next": "15.5.3", "next-auth": "^4.24.11", "next-themes": "^0.4.3", @@ -36,6 +38,9 @@ "react-dom": "^18.2.0", "react-qr-code": "^2.0.18", "react-tweet": "^3.2.1", + "remark": "^15.0.1", + "remark-gfm": "^4.0.1", + "remark-html": "^16.0.1", "superjson": "^2.2.5", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", @@ -43,6 +48,7 @@ }, "devDependencies": { "@types/dompurify": "^3.2.0", + "@tailwindcss/typography": "^0.5.19", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx b/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx new file mode 100644 index 00000000..93e7bafe --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx @@ -0,0 +1,165 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { Search } from "lucide-react"; +import NewsletterItem from "@/components/newsletter/NewsletterItem"; +import CustomDropdown from "@/components/newsletter/Dropdown"; + +export default function NewsletterPage({ + initialData, +}: { + initialData: { + id: string; + title: string; + summary: string; + keywords: string[]; + time: string; + readTime: string; + }[]; +}) { + const [search, setSearch] = useState(""); + const [sortOrder, setSortOrder] = useState("newest"); + const [selectedMonth, setSelectedMonth] = useState("all"); + + const availableMonths = useMemo(() => { + const months = new Set(); + initialData.forEach((item) => { + const date = new Date(item.time); + const monthYear = date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + }); + months.add(monthYear); + }); + return Array.from(months).sort( + (a, b) => new Date(b).getTime() - new Date(a).getTime() + ); + }, [initialData]); + + const monthOptions = [ + { label: "All Months", value: "all" }, + ...availableMonths.map((month) => ({ + label: month, + value: month, + })), + ]; + + const filtered = initialData.filter((item) => { + const text = + `${item.title} ${item.summary} ${item.keywords.join(" ")}`.toLowerCase(); + return text.includes(search.toLowerCase()); + }); + + const filteredByMonth = filtered.filter((item) => { + if (selectedMonth === "all") return true; + + const itemDate = new Date(item.time); + const itemMonthYear = itemDate.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + }); + + return itemMonthYear === selectedMonth; + }); + + const sorted = [...filteredByMonth].sort((a, b) => { + const dateA = new Date(a.time).getTime(); + const dateB = new Date(b.time).getTime(); + return sortOrder === "newest" ? dateB - dateA : dateA - dateB; + }); + + return ( +
+
+
+

+ Newsletter +

+
+ +

+ Stay updated with our latest insights and stories +

+
+ +
+ {/* Search Input */} +
+ + setSearch(e.target.value)} + className=" + w-full bg-white/5 border border-white/10 rounded-lg + px-9 py-2 text-sm text-gray-200 placeholder-gray-400 + focus:outline-none focus:border-purple-400 + transition-all duration-200 + " + /> +
+ +
+
+ +
+ +
+ +
+
+
+ + {(search || selectedMonth !== "all") && ( +
+ {sorted.length} result{sorted.length !== 1 ? "s" : ""} found +
+ )} + +
+ {sorted.length > 0 ? ( + sorted.map((item) => ( + + )) + ) : ( +
+

No newsletters found.

+ {(search || selectedMonth !== "all") && ( + + )} +
+ )} +
+
+ ); +} diff --git a/apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx b/apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx new file mode 100644 index 00000000..dcff1251 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx @@ -0,0 +1,111 @@ +import { notFound } from "next/navigation"; +import { + getAllNewsletterSlugs, + getNewsletterBySlug, +} from "@/utils/newsletter/markdown"; +import { ClockIcon } from "@heroicons/react/24/outline"; +import { CalendarDaysIcon, ChevronLeft } from "lucide-react"; +import Link from "next/link"; + +export async function generateStaticParams() { + return getAllNewsletterSlugs().map((slug) => ({ slug })); +} + +function sanitizeHtml(html: string): string { + return html + .replace(/style="[^"]*width:\s*\d+px[^"]*"/gi, "") + .replace(/style="[^"]*min-width:\s*\d+px[^"]*"/gi, "") + .replace(/width="\d+"/gi, "") + .replace(/min-width="\d+"/gi, ""); +} + +export default async function NewsletterPage({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + + let newsletter; + try { + newsletter = await getNewsletterBySlug(slug); + } catch { + return notFound(); + } + + const sanitizedHtml = sanitizeHtml(newsletter.html); + + return ( +
+
+ + + + Back to newsletters + + + +
+
+

+ {newsletter.metadata.title} +

+ +
+ {newsletter.metadata.date && ( +
+ + +
+ )} + + {newsletter.metadata.readTime && ( +
+ + {newsletter.metadata.readTime} +
+ )} +
+
+ +
+
+
+ +
+
+

+ Hope you enjoyed reading it. +

+ + + © {new Date().getFullYear()} OpenSox + +
+
+
+
+
+ ); +} diff --git a/apps/web/src/app/(main)/dashboard/newsletters/page.tsx b/apps/web/src/app/(main)/dashboard/newsletters/page.tsx new file mode 100644 index 00000000..e9293af0 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/page.tsx @@ -0,0 +1,8 @@ +import { getAllNewsLetter } from "@/utils/newsletter/getNewsLetter"; +import NewsletterPage from "./NewsletterPage"; + +export default function Page() { + const newsletters = getAllNewsLetter(); + + return ; +} diff --git a/apps/web/src/components/dashboard/Sidebar.tsx b/apps/web/src/components/dashboard/Sidebar.tsx index e8770831..f08a7fa8 100644 --- a/apps/web/src/components/dashboard/Sidebar.tsx +++ b/apps/web/src/components/dashboard/Sidebar.tsx @@ -22,6 +22,7 @@ import { signOut, useSession } from "next-auth/react"; import { ProfilePic } from "./ProfilePic"; import { useSubscription } from "@/hooks/useSubscription"; import { OpensoxProBadge } from "../sheet/OpensoxProBadge"; +import { Mailbox } from "lucide-react"; const SIDEBAR_ROUTES = [ { @@ -65,6 +66,10 @@ export default function Sidebar() { } }; + const handleNewsLetter = () => { + router.push("/dashboard/newsletters"); + }; + return (
} collapsed={isCollapsed} /> + {!isCollapsed && !isPaidUser ? ( +
+ + + +
+

+ Newsletter +

+ +
+
+ ) : ( + } + collapsed={isCollapsed} + /> + )} + {!isCollapsed && !isPaidUser ? (
} - collapsed={isCollapsed} - /> + collapsed={isCollapsed} + /> )}
diff --git a/apps/web/src/components/newsletter/Dropdown.tsx b/apps/web/src/components/newsletter/Dropdown.tsx new file mode 100644 index 00000000..63a83a48 --- /dev/null +++ b/apps/web/src/components/newsletter/Dropdown.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { ChevronDown, ChevronUp } from "lucide-react"; + +export default function CustomDropdown({ + value, + onChange, + options, +}: { + value: string; + onChange: (value: string) => void; + options: { label: string; value: string }[]; +}) { + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + function handleClick(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false); + } + } + document.addEventListener("mousedown", handleClick); + return () => document.removeEventListener("mousedown", handleClick); + }, []); + + const selected = options.find((opt) => opt.value === value); + + return ( +
+ + + {open && ( +
+ {options.map((opt) => ( +
{ + onChange(opt.value); + setOpen(false); + }} + className=" + px-3 py-2 text-sm rounded-md text-gray-100 cursor-pointer + hover:bg-neutral-700 transition-all duration-200 + " + > + {opt.label} +
+ ))} +
+ )} +
+ ); +} diff --git a/apps/web/src/components/newsletter/NewsletterItem.tsx b/apps/web/src/components/newsletter/NewsletterItem.tsx new file mode 100644 index 00000000..47bfa5b5 --- /dev/null +++ b/apps/web/src/components/newsletter/NewsletterItem.tsx @@ -0,0 +1,81 @@ +import { Clock2Icon } from "lucide-react"; + +interface NewsletterItemProps { + title: string; + time: string; + readTime?: string; + summary?: string; + keywords?: string[]; +} + +export default function NewsletterItem({ + time, + title, + summary, + keywords, + readTime, +}: NewsletterItemProps) { + return ( +
+
+
+ + {title} + + +
+ + {new Date(time).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + })} + + + {readTime && ( + + + {readTime} + + )} +
+
+ + {summary && ( +

+ {summary} +

+ )} + + {keywords && keywords.length > 0 && ( +
+ {keywords.map((keyword, idx) => ( + + {keyword} + + ))} +
+ )} +
+
+ ); +} diff --git a/apps/web/src/utils/newsletter/getNewsLetter.ts b/apps/web/src/utils/newsletter/getNewsLetter.ts new file mode 100644 index 00000000..cf16fd1f --- /dev/null +++ b/apps/web/src/utils/newsletter/getNewsLetter.ts @@ -0,0 +1,42 @@ +import fs from "fs"; +import path from "path"; +import matter from "gray-matter"; + +export type Newsletter = { + id: string; + title: string; + summary: string; + keywords: string[]; + time: string; + readTime: string; +}; + +const newsletterDir = path.join(process.cwd(), "content/newsletters"); + +export function getAllNewsLetter(): Newsletter[] { + const files = fs.readdirSync(newsletterDir); + + return files + .filter((file) => file.endsWith(".md") || file.endsWith(".mdx")) + .map((filename) => { + const filePath = path.join(newsletterDir, filename); + const raw = fs.readFileSync(filePath, "utf8"); + const { data } = matter(raw); + + // Normalize keywords + const keywords = Array.isArray(data.keywords) + ? data.keywords + : typeof data.keywords === "string" + ? data.keywords.split(",").map((k: string) => k.trim()) + : []; + + return { + id: data.id ?? filename.replace(/\.mdx?$/, ""), + title: data.title ?? "Untitled Newsletter", + summary: data.summary ?? "", + keywords, + time: data.time ?? data.date ?? new Date().toISOString(), + readTime: data.readTime ?? "1 min read", + } satisfies Newsletter; + }); +} diff --git a/apps/web/src/utils/newsletter/markdown.ts b/apps/web/src/utils/newsletter/markdown.ts new file mode 100644 index 00000000..59ac33c1 --- /dev/null +++ b/apps/web/src/utils/newsletter/markdown.ts @@ -0,0 +1,32 @@ +import fs from "fs"; +import path from "path"; +import matter from "gray-matter"; +import { remark } from "remark"; +import html from "remark-html"; +import gfm from "remark-gfm"; + +const newsletterDir = path.join(process.cwd(), "content/newsletters"); + +export function getAllNewsletterSlugs(): string[] { + return fs + .readdirSync(newsletterDir) + .filter((f) => f.endsWith(".md")) + .map((f) => f.replace(/\.md$/, "")); +} + +export async function getNewsletterBySlug(slug: string): Promise<{ + metadata: Record; + html: string; +}> { + const fullPath = path.join(newsletterDir, `${slug}.md`); + const file = fs.readFileSync(fullPath, "utf8"); + + const { data, content } = matter(file); + + const processed = await remark().use(gfm).use(html).process(content); + + return { + metadata: data, + html: processed.toString(), + }; +} diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts index a5074ca3..f0ebe250 100644 --- a/apps/web/tailwind.config.ts +++ b/apps/web/tailwind.config.ts @@ -1,4 +1,6 @@ import type { Config } from "tailwindcss"; +import tailwindcssAnimate from "tailwindcss-animate"; +import tailwindcssTypography from "@tailwindcss/typography"; const config: Config = { darkMode: ["class"], @@ -120,9 +122,95 @@ const config: Config = { DMfont: ["var(--font-dm-mono-med)", "var(--font-dm-mono-req)"], MonaSans: ["var(--font-mona-sans-med)", "var(--font-mona-sans-req)"], }, + typography: { + DEFAULT: { + css: { + maxWidth: "none", + color: "#e5e7eb", + a: { + color: "#9455f4", + "&:hover": { + color: "#7A45C3", + }, + textDecoration: "none", + }, + strong: { + color: "#ffffff", + fontWeight: "600", + }, + code: { + color: "#ec4899", + backgroundColor: "#1f2937", + padding: "0.25rem 0.375rem", + borderRadius: "0.25rem", + fontWeight: "400", + }, + "code::before": { + content: '""', + }, + "code::after": { + content: '""', + }, + pre: { + backgroundColor: "#0f172a", + color: "#e5e7eb", + borderRadius: "0.5rem", + padding: "1rem", + }, + "pre code": { + backgroundColor: "transparent", + padding: "0", + color: "inherit", + }, + h1: { + color: "#ffffff", + fontWeight: "700", + }, + h2: { + color: "#ffffff", + fontWeight: "700", + }, + h3: { + color: "#ffffff", + fontWeight: "600", + }, + h4: { + color: "#ffffff", + fontWeight: "600", + }, + blockquote: { + color: "#9ca3af", + borderLeftColor: "#9455f4", + fontStyle: "italic", + }, + hr: { + borderColor: "#252525", + }, + img: { + borderRadius: "0.5rem", + boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)", + }, + table: { + width: "100%", + }, + thead: { + backgroundColor: "#1f2937", + }, + "thead th": { + color: "#ffffff", + fontWeight: "600", + padding: "0.75rem 1rem", + }, + "tbody td": { + padding: "0.75rem 1rem", + borderColor: "#252525", + }, + }, + }, + }, }, }, - plugins: [require("tailwindcss-animate")], + plugins: [tailwindcssAnimate, tailwindcssTypography], }; export default config; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6fa2dbc..c3a6e9b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -181,9 +181,15 @@ importers: geist: specifier: ^1.5.1 version: 1.5.1(next@15.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + gray-matter: + specifier: ^4.0.3 + version: 4.0.3 lucide-react: specifier: ^0.456.0 version: 0.456.0(react@18.3.1) + marked: + specifier: ^17.0.0 + version: 17.0.0 next: specifier: 15.5.3 version: 15.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -208,6 +214,15 @@ importers: react-tweet: specifier: ^3.2.1 version: 3.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + remark: + specifier: ^15.0.1 + version: 15.0.1 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 + remark-html: + specifier: ^16.0.1 + version: 16.0.1 superjson: specifier: ^2.2.5 version: 2.2.5 @@ -221,9 +236,15 @@ importers: specifier: ^5.0.1 version: 5.0.5(@types/react@18.3.23)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)) devDependencies: +<<<<<<< HEAD '@types/dompurify': specifier: ^3.2.0 version: 3.2.0 +======= + '@tailwindcss/typography': + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.19.0)(typescript@5.9.2))) +>>>>>>> d530366 (add: added the newsletter listing page, and the newsletter page, markdown exporter function) '@types/node': specifier: ^20 version: 20.19.0 @@ -1221,6 +1242,11 @@ packages: '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@tanstack/query-core@5.90.2': resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} @@ -1285,9 +1311,14 @@ packages: '@types/cors@2.8.19': resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} +<<<<<<< HEAD '@types/dompurify@3.2.0': resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. +======= + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} +>>>>>>> d530366 (add: added the newsletter listing page, and the newsletter page, markdown exporter function) '@types/eslint@8.56.12': resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==} @@ -1304,6 +1335,9 @@ packages: '@types/glob@7.2.0': resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} @@ -1319,6 +1353,9 @@ packages: '@types/jsonwebtoken@9.0.9': resolution: {integrity: sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} @@ -1372,8 +1409,13 @@ packages: '@types/tinycolor2@1.4.6': resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} +<<<<<<< HEAD '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} +======= + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} +>>>>>>> d530366 (add: added the newsletter listing page, and the newsletter page, markdown exporter function) '@typescript-eslint/eslint-plugin@6.21.0': resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} @@ -1940,6 +1982,9 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2030,6 +2075,9 @@ packages: caniuse-lite@1.0.30001731: resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -2045,6 +2093,15 @@ packages: change-case@3.1.0: resolution: {integrity: sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw==} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} @@ -2117,6 +2174,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -2230,6 +2290,9 @@ packages: supports-color: optional: true + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -2296,6 +2359,9 @@ packages: resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -2421,6 +2487,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + escodegen@2.1.0: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} engines: {node: '>=6.0'} @@ -2672,6 +2742,13 @@ packages: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -2912,6 +2989,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -2948,6 +3029,15 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-sanitize@5.0.2: + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + header-case@1.0.1: resolution: {integrity: sha512-i0q9mkOeSuhXw6bGgiQCCBgY/jlZuV/7dZXyZ9c6LcBrqwvT8eT719E9uxE5LiZftdl+z81Ugbg/VvXV4OJOeQ==} @@ -2962,6 +3052,9 @@ packages: hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -3084,6 +3177,10 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3289,6 +3386,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -3354,6 +3455,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -3389,10 +3493,54 @@ packages: make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + marked@17.0.0: + resolution: {integrity: sha512-KkDYEWEEiYJw/KC+DVm1zzlpMQSMIu6YRltkcCvwheCp8HWPXCk9JwOmHJKBlGfzcpzcIt6x3sMnTsRm/51oDg==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -3411,6 +3559,90 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -3824,6 +4056,10 @@ packages: peerDependencies: postcss: ^8.2.14 + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + postcss-selector-parser@6.1.2: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} @@ -3886,6 +4122,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -3991,6 +4230,21 @@ packages: resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} hasBin: true + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-html@16.0.1: + resolution: {integrity: sha512-B9JqA5i0qZe0Nsf49q3OXyGvyXuZFDzAP2iOFLEumymuYJITVpiH1IgsTEwTpdptDmZlMDMWeDmSawdaJIGCXQ==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + remark@15.0.1: + resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -4073,6 +4327,10 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + semver-compare@1.0.0: resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} @@ -4188,6 +4446,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -4255,6 +4516,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -4263,6 +4527,10 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -4396,6 +4664,15 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} +<<<<<<< HEAD +======= + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + +>>>>>>> d530366 (add: added the newsletter listing page, and the newsletter page, markdown exporter function) ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -4544,6 +4821,24 @@ packages: undici-types@7.12.0: resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} @@ -4606,6 +4901,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -4724,6 +5025,9 @@ packages: use-sync-external-store: optional: true + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -5517,6 +5821,11 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.8.1 + '@tailwindcss/typography@0.5.19(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.19.0)(typescript@5.9.2)))': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@20.19.0)(typescript@5.9.2)) + '@tanstack/query-core@5.90.2': {} '@tanstack/react-query@5.90.2(react@18.3.1)': @@ -5605,9 +5914,15 @@ snapshots: dependencies: '@types/node': 20.19.0 +<<<<<<< HEAD '@types/dompurify@3.2.0': dependencies: dompurify: 3.3.0 +======= + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 +>>>>>>> d530366 (add: added the newsletter listing page, and the newsletter page, markdown exporter function) '@types/eslint@8.56.12': dependencies: @@ -5635,6 +5950,10 @@ snapshots: '@types/minimatch': 3.0.5 '@types/node': 20.19.0 + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/http-errors@2.0.5': {} '@types/inquirer@6.5.0': @@ -5651,6 +5970,10 @@ snapshots: '@types/ms': 2.1.0 '@types/node': 20.19.0 + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/mime@1.3.5': {} '@types/minimatch@3.0.5': {} @@ -5703,8 +6026,12 @@ snapshots: '@types/tinycolor2@1.4.6': {} +<<<<<<< HEAD '@types/trusted-types@2.0.7': optional: true +======= + '@types/unist@3.0.3': {} +>>>>>>> d530366 (add: added the newsletter listing page, and the newsletter page, markdown exporter function) '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)': dependencies: @@ -6348,6 +6675,8 @@ snapshots: axobject-query@4.1.0: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -6448,6 +6777,8 @@ snapshots: caniuse-lite@1.0.30001731: {} + ccount@2.0.1: {} + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -6485,6 +6816,12 @@ snapshots: upper-case: 1.1.3 upper-case-first: 1.1.2 + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + chardet@0.7.0: {} chokidar@3.6.0: @@ -6559,6 +6896,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + commander@10.0.1: {} commander@4.1.1: {} @@ -6651,6 +6990,10 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.2.0: + dependencies: + character-entities: 2.0.2 + deep-extend@0.6.0: {} deep-is@0.1.4: {} @@ -6735,6 +7078,10 @@ snapshots: detect-newline@4.0.1: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + didyoumean@1.2.2: {} diff@4.0.2: {} @@ -6932,6 +7279,8 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + escodegen@2.1.0: dependencies: esprima: 4.0.1 @@ -7421,6 +7770,12 @@ snapshots: transitivePeerDependencies: - supports-color + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend@3.0.2: {} + external-editor@3.1.0: dependencies: chardet: 0.7.0 @@ -7704,6 +8059,13 @@ snapshots: graphemer@1.4.0: {} + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -7737,6 +8099,30 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-sanitize@5.0.2: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.0 + unist-util-position: 5.0.0 + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + header-case@1.0.1: dependencies: no-case: 2.3.2 @@ -7750,6 +8136,8 @@ snapshots: hosted-git-info@2.8.9: {} + html-void-elements@3.0.0: {} + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -7906,6 +8294,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -8104,6 +8494,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kind-of@6.0.3: {} + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -8156,6 +8548,8 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -8188,8 +8582,126 @@ snapshots: make-error@1.3.6: {} + markdown-table@3.0.4: {} + + marked@17.0.0: {} + math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + media-typer@0.3.0: {} merge-descriptors@1.0.3: {} @@ -8200,6 +8712,197 @@ snapshots: methods@1.1.2: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.1 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -8623,6 +9326,11 @@ snapshots: postcss: 8.5.5 postcss-selector-parser: 6.1.2 + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 @@ -8681,6 +9389,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.1.0: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -8821,6 +9531,49 @@ snapshots: dependencies: jsesc: 0.5.0 + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-html@16.0.1: + dependencies: + '@types/mdast': 4.0.4 + hast-util-sanitize: 5.0.2 + hast-util-to-html: 9.0.5 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + remark@15.0.1: + dependencies: + '@types/mdast': 4.0.4 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + require-directory@2.1.1: {} require-package-name@2.0.1: {} @@ -8905,6 +9658,11 @@ snapshots: dependencies: loose-envify: 1.4.0 + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + semver-compare@1.0.0: {} semver@5.7.2: {} @@ -9079,6 +9837,8 @@ snapshots: source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -9174,6 +9934,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -9182,6 +9947,8 @@ snapshots: dependencies: ansi-regex: 6.1.0 + strip-bom-string@1.0.0: {} + strip-bom@3.0.0: {} strip-final-newline@2.0.0: {} @@ -9317,6 +10084,13 @@ snapshots: tr46@0.0.3: {} +<<<<<<< HEAD +======= + trim-lines@3.0.1: {} + + trough@2.2.0: {} + +>>>>>>> d530366 (add: added the newsletter listing page, and the newsletter page, markdown exporter function) ts-api-utils@1.4.3(typescript@5.9.2): dependencies: typescript: 5.9.2 @@ -9463,6 +10237,39 @@ snapshots: undici-types@7.12.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universal-user-agent@7.0.3: {} universalify@2.0.1: {} @@ -9535,6 +10342,16 @@ snapshots: vary@1.1.2: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + wcwidth@1.0.1: dependencies: defaults: 1.0.4 @@ -9660,3 +10477,5 @@ snapshots: '@types/react': 18.3.23 react: 18.3.1 use-sync-external-store: 1.5.0(react@18.3.1) + + zwitch@2.0.4: {} From 2b2d35ea28c19c2ed2a989928b80d79c886cd98a Mon Sep 17 00:00:00 2001 From: Devesh Chandra Date: Sun, 16 Nov 2025 16:09:29 +0530 Subject: [PATCH 02/14] add: added routing on the newsletter click --- apps/web/content/newsletters/test.md | 2 +- .../dashboard/newsletters/NewsletterPage.tsx | 33 ++++++++----------- .../components/newsletter/NewsletterItem.tsx | 4 +++ .../web/src/utils/newsletter/getNewsLetter.ts | 7 ++-- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/apps/web/content/newsletters/test.md b/apps/web/content/newsletters/test.md index 4f8d1e37..5f724d98 100644 --- a/apps/web/content/newsletters/test.md +++ b/apps/web/content/newsletters/test.md @@ -5,7 +5,7 @@ date: "2025-11-16" summary: "A brief 1-2 sentence summary of what this newsletter is about. This will appear below the title." keywords: ["keyword1", "keyword2", "keyword3"] readTime: "5 min read" -slug: "your-newsletter-slug" +slug: "test" --- ## Add the content here diff --git a/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx b/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx index 93e7bafe..0c728ba6 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx @@ -15,6 +15,7 @@ export default function NewsletterPage({ keywords: string[]; time: string; readTime: string; + slug: string; }[]; }) { const [search, setSearch] = useState(""); @@ -52,13 +53,11 @@ export default function NewsletterPage({ const filteredByMonth = filtered.filter((item) => { if (selectedMonth === "all") return true; - const itemDate = new Date(item.time); const itemMonthYear = itemDate.toLocaleDateString("en-US", { year: "numeric", month: "long", }); - return itemMonthYear === selectedMonth; }); @@ -71,20 +70,16 @@ export default function NewsletterPage({ return (
-
-

- Newsletter -

-
- +

+ Newsletter +

Stay updated with our latest insights and stories

-
- {/* Search Input */} -
+
+
setSearch(e.target.value)} className=" - w-full bg-white/5 border border-white/10 rounded-lg - px-9 py-2 text-sm text-gray-200 placeholder-gray-400 - focus:outline-none focus:border-purple-400 - transition-all duration-200 - " + w-full bg-white/5 border border-white/10 rounded-lg + px-9 py-2 text-sm text-gray-200 placeholder-gray-400 + focus:outline-none focus:border-purple-400 + transition-all duration-200 + " />
-
-
+
+
-
)) ) : ( diff --git a/apps/web/src/components/newsletter/NewsletterItem.tsx b/apps/web/src/components/newsletter/NewsletterItem.tsx index 47bfa5b5..1957628f 100644 --- a/apps/web/src/components/newsletter/NewsletterItem.tsx +++ b/apps/web/src/components/newsletter/NewsletterItem.tsx @@ -1,4 +1,5 @@ import { Clock2Icon } from "lucide-react"; +import { redirect } from "next/navigation"; interface NewsletterItemProps { title: string; @@ -6,6 +7,7 @@ interface NewsletterItemProps { readTime?: string; summary?: string; keywords?: string[]; + slug: string; } export default function NewsletterItem({ @@ -14,6 +16,7 @@ export default function NewsletterItem({ summary, keywords, readTime, + slug, }: NewsletterItemProps) { return (
redirect(`/dashboard/newsletters/${slug}`)} >
diff --git a/apps/web/src/utils/newsletter/getNewsLetter.ts b/apps/web/src/utils/newsletter/getNewsLetter.ts index cf16fd1f..ce588f97 100644 --- a/apps/web/src/utils/newsletter/getNewsLetter.ts +++ b/apps/web/src/utils/newsletter/getNewsLetter.ts @@ -9,6 +9,7 @@ export type Newsletter = { keywords: string[]; time: string; readTime: string; + slug: string; }; const newsletterDir = path.join(process.cwd(), "content/newsletters"); @@ -23,7 +24,8 @@ export function getAllNewsLetter(): Newsletter[] { const raw = fs.readFileSync(filePath, "utf8"); const { data } = matter(raw); - // Normalize keywords + const slug = filename.replace(/\.mdx?$/, ""); + const keywords = Array.isArray(data.keywords) ? data.keywords : typeof data.keywords === "string" @@ -31,12 +33,13 @@ export function getAllNewsLetter(): Newsletter[] { : []; return { - id: data.id ?? filename.replace(/\.mdx?$/, ""), + id: data.id ?? slug, title: data.title ?? "Untitled Newsletter", summary: data.summary ?? "", keywords, time: data.time ?? data.date ?? new Date().toISOString(), readTime: data.readTime ?? "1 min read", + slug, } satisfies Newsletter; }); } From 9fcd49ddc93a53451db912fb3f582386ad07d70b Mon Sep 17 00:00:00 2001 From: Devesh Chandra Date: Sun, 16 Nov 2025 16:39:40 +0530 Subject: [PATCH 03/14] add: added the loading skeleton, and only pro user can access the newsletter --- .../dashboard/newsletters/[slug]/layout.tsx | 31 +++++++++++ .../newsletter/NewsletterSkeleton.tsx | 54 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx create mode 100644 apps/web/src/components/newsletter/NewsletterSkeleton.tsx diff --git a/apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx b/apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx new file mode 100644 index 00000000..13234f58 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import NewsletterSkeleton from "@/components/newsletter/NewsletterSkeleton"; +import { useSubscription } from "@/hooks/useSubscription"; + +export default function NewsletterLayout({ + children, +}: { + children: React.ReactNode; +}) { + const router = useRouter(); + const { isLoading, isPaidUser } = useSubscription(); + + useEffect(() => { + if (!isLoading && !isPaidUser) { + router.replace("/pricing"); + } + }, []); + + if (isLoading) { + return ; + } + + if (!isPaidUser) { + return null; + } + + return <>{children}; +} diff --git a/apps/web/src/components/newsletter/NewsletterSkeleton.tsx b/apps/web/src/components/newsletter/NewsletterSkeleton.tsx new file mode 100644 index 00000000..c835bc40 --- /dev/null +++ b/apps/web/src/components/newsletter/NewsletterSkeleton.tsx @@ -0,0 +1,54 @@ +export default function NewsletterSkeleton() { + return ( +
+
+ {/* Back link skeleton */} +
+
+
+
+ +
+
+ {/* Title skeleton */} +
+
+
+
+ + {/* Meta info skeleton */} +
+
+
+
+
+ +
+
+
+
+
+
+ + {/* Body skeleton */} +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+ ))} +
+
+ +
+
+
+
+
+
+
+
+
+ ); +} From c88532d0718c232cdbba60df2f0032c7324c9ee1 Mon Sep 17 00:00:00 2001 From: Devesh Chandra Date: Sun, 16 Nov 2025 16:42:50 +0530 Subject: [PATCH 04/14] fix: add loadingstate, and user sub to the dependecies array --- apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx b/apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx index 13234f58..040f5fbc 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx @@ -17,7 +17,7 @@ export default function NewsletterLayout({ if (!isLoading && !isPaidUser) { router.replace("/pricing"); } - }, []); + }, [isLoading, isPaidUser, router]); if (isLoading) { return ; From c9ec8cba74e3232362c9987914486568137a3679 Mon Sep 17 00:00:00 2001 From: Devesh Chandra Date: Sun, 16 Nov 2025 17:47:32 +0530 Subject: [PATCH 05/14] rename: update the name of some files --- .../newsletters/{test.md => example.md} | 0 apps/web/content/newsletters/test-1.md | 63 ++++++++++++ apps/web/content/newsletters/test-2.md | 68 +++++++++++++ .../dashboard/newsletters/NewsletterPage.tsx | 98 ++++++------------- .../dashboard/newsletters/[slug]/layout.tsx | 4 +- .../dashboard/newsletters/[slug]/page.tsx | 2 +- .../app/(main)/dashboard/newsletters/page.tsx | 2 +- apps/web/src/hooks/useNewsletterFilters.ts | 92 +++++++++++++++++ .../{getNewsLetter.ts => getAllNewsLetter.ts} | 7 +- .../{markdown.ts => getNewsletterBySlug.ts} | 0 10 files changed, 265 insertions(+), 71 deletions(-) rename apps/web/content/newsletters/{test.md => example.md} (100%) create mode 100644 apps/web/content/newsletters/test-1.md create mode 100644 apps/web/content/newsletters/test-2.md create mode 100644 apps/web/src/hooks/useNewsletterFilters.ts rename apps/web/src/utils/newsletter/{getNewsLetter.ts => getAllNewsLetter.ts} (83%) rename apps/web/src/utils/newsletter/{markdown.ts => getNewsletterBySlug.ts} (100%) diff --git a/apps/web/content/newsletters/test.md b/apps/web/content/newsletters/example.md similarity index 100% rename from apps/web/content/newsletters/test.md rename to apps/web/content/newsletters/example.md diff --git a/apps/web/content/newsletters/test-1.md b/apps/web/content/newsletters/test-1.md new file mode 100644 index 00000000..90112f7f --- /dev/null +++ b/apps/web/content/newsletters/test-1.md @@ -0,0 +1,63 @@ +--- +id: "from-farm-to-founder" +title: "From Farm Boy to Founder: A Journey of Grit and Code" +date: "2025-10-23" +summary: "How one developer went from being a hotel receptionist with no college degree to building a startup with 10,000 users and multiple investment offers—all through self-taught programming and relentless determination." +keywords: + [ + "entrepreneurship", + "self-taught developer", + "startup journey", + "open source", + "career transition", + ] +readTime: "4 min read" +slug: "from-farm-to-founder" +--- + +![Journey from farm to founder](https://imgs.search.brave.com/0NvT2qcazrFJeTKxSWwaaSCfq2Ak2DCGgdHPup131xQ/rs:fit:860:0:0:0/g:ce/aHR0cHM6Ly93YWxs/cGFwZXJzLmNvbS9p/bWFnZXMvZmVhdHVy/ZWQvaGFyZC13b3Jr/LXBpY3R1cmVzLTMw/YjY2amNnc203aGw2/djIuanBn) + +## The Beginning: Breaking Free + +I was born and raised on a farm. Despite nailing my studies, I got rejected by the air force. My parents, like many Indian parents, pushed hard for a government job—the "safe" option. At 18, I made a choice that would change everything: I left home. + +My first job? A receptionist at a hotel. Not glamorous. Not prestigious. But it was _mine_. + +## The Pivot: A Computer and a Dream + +I bought a computer with my savings. Late nights after hotel shifts, I taught myself programming. No college degree. No connections. No safety net. Just documentation, tutorials, and an obsessive drive to learn. + +Eventually, I quit the hotel job and started from absolute scratch in tech. + +## The Grind: Open Source and GSoC + +I dove into open source. My first Google Summer of Code application? **Rejected.** + +But I didn't stop. I worked harder, contributed more, made genuine friends in the community. The second time around, I didn't just get in—I excelled. Internships followed. Job offers came. I even became a **GSoC mentor** myself. + +While still learning and growing, I started earning lakhs. I paid my brother's college fees. I covered my father's medical treatment. The farm boy who left home at 18 was now supporting his family. + +## The Leap: Building Something Real + +Despite the stable job and good income, I couldn't shake the itch to build something of my own. I started a side project. It grew to **10,000 users**. Then came **2 investment offers**. + +The numbers spoke for themselves: my side project revenue crossed my salary. I left the job. + +Now I'm scaling [opensox.ai](http://opensox.ai), and the journey continues. + +## The Lesson: You Can Just Do Things + +There's no secret formula. No hidden shortcut. No "right" background. + +What worked for me: + +- **Self-belief over credentials** – No college degree couldn't stop me from learning to code +- **Community over isolation** – Open source connected me with people who became mentors and friends +- **Action over permission** – I didn't wait for someone to give me a chance; I created my own +- **Persistence over perfection** – Failed GSoC? Applied again. Got rejected? Kept building. + +If you're reading this from a small town, from a non-tech background, from a place where startup dreams seem impossible—know this: **You can just do things.** + +You don't need permission. You don't need a fancy degree. You need a computer, internet, and the refusal to quit. + +_Want to follow my journey? Check out what we're building at [opensox.ai](http://opensox.ai)_ diff --git a/apps/web/content/newsletters/test-2.md b/apps/web/content/newsletters/test-2.md new file mode 100644 index 00000000..ece9a85f --- /dev/null +++ b/apps/web/content/newsletters/test-2.md @@ -0,0 +1,68 @@ +--- +id: "ugly-execution" +title: "Ugly Execution: Why Imperfect Action Beats Perfect Planning" +date: "2025-11-16" +summary: "Stop waiting for perfect conditions. Learn why taking messy, imperfect action is infinitely more valuable than endless planning and preparation." +keywords: + [ + "productivity", + "mindset", + "execution", + "perfectionism", + "taking action", + "startup advice", + ] +readTime: "3 min read" +slug: "ugly-execution" +--- + +![Taking imperfect action](https://pbs.twimg.com/media/G5aMbWTbcAEr9vp?format=jpg&name=medium) + +I use the term **"ugly execution"** to define a state when you don't care about the perfectness in anything and just act. + +When you understand that **life rewards action, not perfection.** + +## What Ugly Execution Looks Like + +When your camera sucks, but still you hit that red button, record a scrappy and embarrassing video, and click on "post" and actually post it on YouTube. + +When your website's UI sucks, authentication fails, gets "build errors" randomly, and lags like government websites, but you still publish it and let people use it. + +When your resume looks like a blank slate, but you still apply for the job you wanted. + +## The Uncomfortable Truth + +Your inner self is uncomfortable doing that, but you know that this **"chase of perfection" has the potential to kill your dreams.** + +You know that if you don't act now, your net output in the future will be zero. + +You know, acting, getting stuck, failing, making a stupid mistake, getting embarrassed, is **way better** than creating a mental prison for yourself by useless thinking, worrying, and planning. + +## Stop Chasing Aesthetics + +Don't wait for the perfect: + +- Time +- Camera +- Mic +- Laptop +- Place +- Circumstances +- Website +- People + +You get the point. + +## The Only Rule That Matters + +**JUST START WITH WHATEVER YOU HAVE** + +**LET THE EXECUTION BE UGLY** + +**JUST BE GOOD AT UGLY EXECUTION** + +**LIFE WILL REWARD YOU EXPONENTIALLY.** + +--- + +_The difference between successful people and dreamers? Successful people ship ugly v1s. Dreamers wait for perfect v10s that never come._ diff --git a/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx b/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx index 0c728ba6..9d40f8a3 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx @@ -1,71 +1,38 @@ "use client"; -import { useState, useMemo } from "react"; import { Search } from "lucide-react"; import NewsletterItem from "@/components/newsletter/NewsletterItem"; import CustomDropdown from "@/components/newsletter/Dropdown"; +import { useNewsletterFilters } from "@/hooks/useNewsletterFilters"; + +interface NewsletterData { + id: string; + title: string; + summary: string; + keywords: string[]; + time: string; + readTime: string; + slug: string; +} export default function NewsletterPage({ initialData, }: { - initialData: { - id: string; - title: string; - summary: string; - keywords: string[]; - time: string; - readTime: string; - slug: string; - }[]; + initialData: NewsletterData[]; }) { - const [search, setSearch] = useState(""); - const [sortOrder, setSortOrder] = useState("newest"); - const [selectedMonth, setSelectedMonth] = useState("all"); - - const availableMonths = useMemo(() => { - const months = new Set(); - initialData.forEach((item) => { - const date = new Date(item.time); - const monthYear = date.toLocaleDateString("en-US", { - year: "numeric", - month: "long", - }); - months.add(monthYear); - }); - return Array.from(months).sort( - (a, b) => new Date(b).getTime() - new Date(a).getTime() - ); - }, [initialData]); - - const monthOptions = [ - { label: "All Months", value: "all" }, - ...availableMonths.map((month) => ({ - label: month, - value: month, - })), - ]; - - const filtered = initialData.filter((item) => { - const text = - `${item.title} ${item.summary} ${item.keywords.join(" ")}`.toLowerCase(); - return text.includes(search.toLowerCase()); - }); - - const filteredByMonth = filtered.filter((item) => { - if (selectedMonth === "all") return true; - const itemDate = new Date(item.time); - const itemMonthYear = itemDate.toLocaleDateString("en-US", { - year: "numeric", - month: "long", - }); - return itemMonthYear === selectedMonth; - }); - - const sorted = [...filteredByMonth].sort((a, b) => { - const dateA = new Date(a.time).getTime(); - const dateB = new Date(b.time).getTime(); - return sortOrder === "newest" ? dateB - dateA : dateA - dateB; - }); + const { + search, + setSearch, + sortOrder, + setSortOrder, + selectedMonth, + setSelectedMonth, + monthOptions, + filteredAndSorted, + clearFilters, + hasActiveFilters, + resultCount, + } = useNewsletterFilters(initialData); return (
@@ -119,15 +86,15 @@ export default function NewsletterPage({
- {(search || selectedMonth !== "all") && ( + {hasActiveFilters && (
- {sorted.length} result{sorted.length !== 1 ? "s" : ""} found + {resultCount} result{resultCount !== 1 ? "s" : ""} found
)}
- {sorted.length > 0 ? ( - sorted.map((item) => ( + {filteredAndSorted.length > 0 ? ( + filteredAndSorted.map((item) => (

No newsletters found.

- {(search || selectedMonth !== "all") && ( + {hasActiveFilters && (
); -} +} \ No newline at end of file diff --git a/apps/web/src/components/newsletter/Dropdown.tsx b/apps/web/src/components/newsletter/Dropdown.tsx index 63a83a48..34dea6ba 100644 --- a/apps/web/src/components/newsletter/Dropdown.tsx +++ b/apps/web/src/components/newsletter/Dropdown.tsx @@ -14,6 +14,8 @@ export default function CustomDropdown({ }) { const [open, setOpen] = useState(false); const ref = useRef(null); + const buttonRef = useRef(null); + const optionsRef = useRef<(HTMLButtonElement | null)[]>([]); useEffect(() => { function handleClick(e: MouseEvent) { @@ -25,16 +27,74 @@ export default function CustomDropdown({ return () => document.removeEventListener("mousedown", handleClick); }, []); + // Handle keyboard navigation + useEffect(() => { + if (!open) return; + + function handleKeyDown(e: KeyboardEvent) { + const currentIndex = optionsRef.current.findIndex( + (el) => el === document.activeElement + ); + + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + const nextIndex = (currentIndex + 1) % options.length; + optionsRef.current[nextIndex]?.focus(); + break; + case "ArrowUp": + e.preventDefault(); + const prevIndex = + currentIndex <= 0 ? options.length - 1 : currentIndex - 1; + optionsRef.current[prevIndex]?.focus(); + break; + case "Escape": + setOpen(false); + buttonRef.current?.focus(); + break; + case "Home": + e.preventDefault(); + optionsRef.current[0]?.focus(); + break; + case "End": + e.preventDefault(); + optionsRef.current[options.length - 1]?.focus(); + break; + } + } + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [open, options.length]); + + // Focus first option when dropdown opens + useEffect(() => { + if (open) { + optionsRef.current[0]?.focus(); + } + }, [open]); + const selected = options.find((opt) => opt.value === value); return (
+ ))}
)}
); -} +} \ No newline at end of file diff --git a/apps/web/src/components/newsletter/NewsletterItem.tsx b/apps/web/src/components/newsletter/NewsletterItem.tsx index 1957628f..f9109c56 100644 --- a/apps/web/src/components/newsletter/NewsletterItem.tsx +++ b/apps/web/src/components/newsletter/NewsletterItem.tsx @@ -1,5 +1,5 @@ import { Clock2Icon } from "lucide-react"; -import { redirect } from "next/navigation"; +import Link from "next/link"; interface NewsletterItemProps { title: string; @@ -19,7 +19,8 @@ export default function NewsletterItem({ slug, }: NewsletterItemProps) { return ( -
redirect(`/dashboard/newsletters/${slug}`)} >
@@ -80,6 +81,6 @@ export default function NewsletterItem({
)}
-
+ ); -} +} \ No newline at end of file diff --git a/apps/web/src/hooks/useNewsletterFilters.ts b/apps/web/src/hooks/useNewsletterFilters.ts index f23182d8..dff6f3d8 100644 --- a/apps/web/src/hooks/useNewsletterFilters.ts +++ b/apps/web/src/hooks/useNewsletterFilters.ts @@ -1,16 +1,9 @@ +import { Newsletter } from "@/utils/newsletter/getAllNewsLetter"; import { useState, useMemo } from "react"; -interface NewsletterItem { - id: string; - title: string; - summary: string; - keywords: string[]; - time: string; - readTime: string; - slug: string; -} -export function useNewsletterFilters(initialData: NewsletterItem[]) { + +export function useNewsletterFilters(initialData: Newsletter[]) { const [search, setSearch] = useState(""); const [sortOrder, setSortOrder] = useState("newest"); const [selectedMonth, setSelectedMonth] = useState("all"); diff --git a/apps/web/src/utils/newsletter/getAllNewsLetter.ts b/apps/web/src/utils/newsletter/getAllNewsLetter.ts index f653a6b2..7a47dd50 100644 --- a/apps/web/src/utils/newsletter/getAllNewsLetter.ts +++ b/apps/web/src/utils/newsletter/getAllNewsLetter.ts @@ -12,39 +12,56 @@ export type Newsletter = { slug: string; }; + const newsletterDir = path.join(process.cwd(), "content/newsletters"); export function getAllNewsLetter(): Newsletter[] { - const files = fs.readdirSync(newsletterDir); - - return files - .filter((file) => { - // Skip files starting with "example" - if (file.startsWith("example")) return false; - // Only include .md and .mdx files - return file.endsWith(".md") || file.endsWith(".mdx"); - }) - .map((filename) => { - const filePath = path.join(newsletterDir, filename); - const raw = fs.readFileSync(filePath, "utf8"); - const { data } = matter(raw); - - const slug = filename.replace(/\.mdx?$/, ""); - - const keywords = Array.isArray(data.keywords) - ? data.keywords - : typeof data.keywords === "string" - ? data.keywords.split(",").map((k: string) => k.trim()) - : []; - - return { - id: data.id ?? slug, - title: data.title ?? "Untitled Newsletter", - summary: data.summary ?? "", - keywords, - time: data.time ?? data.date ?? new Date().toISOString(), - readTime: data.readTime ?? "1 min read", - slug, - } satisfies Newsletter; - }); -} + try { + // Check if directory exists + if (!fs.existsSync(newsletterDir)) { + return []; + } + + const files = fs.readdirSync(newsletterDir); + + return files + .filter((file) => { + // Skip files starting with "example" + if (file.startsWith("example")) return false; + // Only include .md files + return file.endsWith(".md"); + }) + .map((filename) => { + try { + const filePath = path.join(newsletterDir, filename); + const raw = fs.readFileSync(filePath, "utf8"); + const { data } = matter(raw); + + const slug = filename.replace(/\.md$/, ""); + + const keywords = Array.isArray(data.keywords) + ? data.keywords + : typeof data.keywords === "string" + ? data.keywords.split(",").map((k: string) => k.trim()) + : []; + + return { + id: data.id ?? slug, + title: data.title ?? "Untitled Newsletter", + summary: data.summary ?? "", + keywords, + time: data.time ?? data.date ?? new Date().toISOString(), + readTime: data.readTime ?? "1 min read", + slug, + } satisfies Newsletter; + } catch (fileError) { + // Silently skip failed files + return null; + } + }) + .filter((newsletter): newsletter is Newsletter => newsletter !== null); + } catch (error) { + // Silently fail and return empty array + return []; + } +} \ No newline at end of file diff --git a/apps/web/src/utils/newsletter/getNewsletterBySlug.ts b/apps/web/src/utils/newsletter/getNewsletterBySlug.ts index 59ac33c1..168b97fe 100644 --- a/apps/web/src/utils/newsletter/getNewsletterBySlug.ts +++ b/apps/web/src/utils/newsletter/getNewsletterBySlug.ts @@ -29,4 +29,4 @@ export async function getNewsletterBySlug(slug: string): Promise<{ metadata: data, html: processed.toString(), }; -} +} \ No newline at end of file From 4bb31b31e915ae66083186a576b3f7898a927aa2 Mon Sep 17 00:00:00 2001 From: Devesh Chandra Date: Sun, 16 Nov 2025 19:40:10 +0000 Subject: [PATCH 09/14] remove the aria code as it was cause too much issue --- apps/web/src/components/newsletter/Dropdown.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/newsletter/Dropdown.tsx b/apps/web/src/components/newsletter/Dropdown.tsx index 34dea6ba..d6729980 100644 --- a/apps/web/src/components/newsletter/Dropdown.tsx +++ b/apps/web/src/components/newsletter/Dropdown.tsx @@ -37,17 +37,19 @@ export default function CustomDropdown({ ); switch (e.key) { - case "ArrowDown": + case "ArrowDown": { e.preventDefault(); const nextIndex = (currentIndex + 1) % options.length; optionsRef.current[nextIndex]?.focus(); break; - case "ArrowUp": + } + case "ArrowUp": { e.preventDefault(); const prevIndex = currentIndex <= 0 ? options.length - 1 : currentIndex - 1; optionsRef.current[prevIndex]?.focus(); break; + } case "Escape": setOpen(false); buttonRef.current?.focus(); @@ -87,9 +89,6 @@ export default function CustomDropdown({ setOpen(true); } }} - aria-haspopup="listbox" - aria-expanded={open} - aria-labelledby="dropdown-label" className=" w-full bg-black/20 border border-white/10 rounded-lg px-3 py-2 text-sm text-gray-200 flex items-center justify-between @@ -106,9 +105,7 @@ export default function CustomDropdown({ {open && ( -
k.trim()) + ? (data.keywords as string).split(",").map((k) => k.trim()) : []; return { - id: data.id ?? slug, - title: data.title ?? "Untitled Newsletter", - summary: data.summary ?? "", + id: (data.id as string | undefined) ?? slug, + title: (data.title as string | undefined) ?? "Untitled Newsletter", + summary: (data.summary as string | undefined) ?? "", keywords, - time: data.time ?? data.date ?? new Date().toISOString(), - readTime: data.readTime ?? "1 min read", + time: + (data.time as string | undefined) ?? + (data.date as string | undefined) ?? + new Date().toISOString(), + readTime: (data.readTime as string | undefined) ?? "1 min read", slug, } satisfies Newsletter; - } catch (fileError) { - // Silently skip failed files + } catch { return null; } }) .filter((newsletter): newsletter is Newsletter => newsletter !== null); - } catch (error) { - // Silently fail and return empty array + } catch { return []; } -} \ No newline at end of file +} From bcf3303cd55d6d12b99997d714c0fb39bac89618 Mon Sep 17 00:00:00 2001 From: Devesh Chandra Date: Sun, 16 Nov 2025 19:56:56 +0000 Subject: [PATCH 11/14] fix: made some recommended changes by coderrabbitai --- .../dashboard/newsletters/NewsletterPage.tsx | 45 +++++++---- apps/web/src/hooks/useNewsletterFilters.ts | 81 ++++++++++--------- 2 files changed, 74 insertions(+), 52 deletions(-) diff --git a/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx b/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx index 7d4d97a8..4ae2bb6c 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx @@ -6,8 +6,6 @@ import CustomDropdown from "@/components/newsletter/Dropdown"; import { useNewsletterFilters } from "@/hooks/useNewsletterFilters"; import { Newsletter } from "@/utils/newsletter/getAllNewsLetter"; - - export default function NewsletterPage({ initialData, }: { @@ -29,20 +27,23 @@ export default function NewsletterPage({ return (
-
+ {/* Header */} +

Newsletter

-

+

Stay updated with our latest insights and stories

-
-
+ {/* Filters */} +
+ {/* Search Input */} +
setSearch(e.target.value)} className=" w-full bg-white/5 border border-white/10 rounded-lg - px-9 py-2 text-sm text-gray-200 placeholder-gray-400 - focus:outline-none focus:border-purple-400 + px-9 py-2.5 text-sm text-gray-200 placeholder-gray-400 + focus:outline-none focus:border-purple-400 focus:ring-2 focus:ring-purple-400/20 transition-all duration-200 " />
+ {/* Dropdown Filters */}
setSortOrder(value as "newest" | "oldest")} options={[ { label: "Newest First", value: "newest" }, { label: "Oldest First", value: "oldest" }, @@ -79,13 +81,15 @@ export default function NewsletterPage({
+ {/* Results Count */} {hasActiveFilters && ( -
+
{resultCount} result{resultCount !== 1 ? "s" : ""} found
)} -
+ {/* Newsletter Items Grid */} +
{filteredAndSorted.length > 0 ? ( filteredAndSorted.map((item) => ( )) ) : ( -
-

No newsletters found.

+
+

+ {hasActiveFilters + ? "No newsletters match your search criteria." + : "No newsletters available."} +

{hasActiveFilters && ( @@ -114,4 +127,4 @@ export default function NewsletterPage({
); -} +} \ No newline at end of file diff --git a/apps/web/src/hooks/useNewsletterFilters.ts b/apps/web/src/hooks/useNewsletterFilters.ts index dff6f3d8..d53c5866 100644 --- a/apps/web/src/hooks/useNewsletterFilters.ts +++ b/apps/web/src/hooks/useNewsletterFilters.ts @@ -1,62 +1,70 @@ -import { Newsletter } from "@/utils/newsletter/getAllNewsLetter"; import { useState, useMemo } from "react"; +import { Newsletter } from "@/utils/newsletter/getAllNewsLetter"; - +type SortOrder = "newest" | "oldest"; export function useNewsletterFilters(initialData: Newsletter[]) { const [search, setSearch] = useState(""); - const [sortOrder, setSortOrder] = useState("newest"); + const [sortOrder, setSortOrder] = useState("newest"); const [selectedMonth, setSelectedMonth] = useState("all"); - const availableMonths = useMemo(() => { + // Generate month options from the data + const monthOptions = useMemo(() => { const months = new Set(); initialData.forEach((item) => { const date = new Date(item.time); const monthYear = date.toLocaleDateString("en-US", { - year: "numeric", month: "long", + year: "numeric", }); months.add(monthYear); }); - return Array.from(months).sort( - (a, b) => new Date(b).getTime() - new Date(a).getTime() - ); - }, [initialData]); - const monthOptions = useMemo( - () => [ + return [ { label: "All Months", value: "all" }, - ...availableMonths.map((month) => ({ - label: month, - value: month, - })), - ], - [availableMonths] - ); + ...Array.from(months) + .sort((a, b) => new Date(b).getTime() - new Date(a).getTime()) + .map((month) => ({ label: month, value: month })), + ]; + }, [initialData]); + // Filter and sort newsletters const filteredAndSorted = useMemo(() => { // Filter by search - const filtered = initialData.filter((item) => { - const text = - `${item.title} ${item.summary} ${item.keywords.join(" ")}`.toLowerCase(); - return text.includes(search.toLowerCase()); + let filtered = initialData.filter((item) => { + const searchLower = search.toLowerCase(); + return ( + item.title.toLowerCase().includes(searchLower) || + item.summary.toLowerCase().includes(searchLower) || + item.keywords.some((keyword) => + keyword.toLowerCase().includes(searchLower) + ) + ); }); // Filter by month - const filteredByMonth = filtered.filter((item) => { - if (selectedMonth === "all") return true; - const itemDate = new Date(item.time); - const itemMonthYear = itemDate.toLocaleDateString("en-US", { - year: "numeric", - month: "long", - }); - return itemMonthYear === selectedMonth; - }); + const filteredByMonth = + selectedMonth === "all" + ? filtered + : filtered.filter((item) => { + const date = new Date(item.time); + const monthYear = date.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + }); + return monthYear === selectedMonth; + }); - // Sort + return [...filteredByMonth].sort((a, b) => { const dateA = new Date(a.time).getTime(); - const dateB = new Date(b.time).getTime(); + const dateB = new Date(b.time).getTime(); + + // Handle invalid dates + if (isNaN(dateA) || isNaN(dateB)) { + return 0; + } + return sortOrder === "newest" ? dateB - dateA : dateA - dateB; }); }, [initialData, search, selectedMonth, sortOrder]); @@ -64,9 +72,11 @@ export function useNewsletterFilters(initialData: Newsletter[]) { const clearFilters = () => { setSearch(""); setSelectedMonth("all"); + setSortOrder("newest"); }; const hasActiveFilters = search !== "" || selectedMonth !== "all"; + const resultCount = filteredAndSorted.length; return { search, @@ -75,11 +85,10 @@ export function useNewsletterFilters(initialData: Newsletter[]) { setSortOrder, selectedMonth, setSelectedMonth, - availableMonths, monthOptions, filteredAndSorted, clearFilters, hasActiveFilters, - resultCount: filteredAndSorted.length, + resultCount, }; -} +} \ No newline at end of file From 147c661ce3e514b2e25b53d29b1088eac6a37a4c Mon Sep 17 00:00:00 2001 From: Devesh Chandra Date: Sun, 16 Nov 2025 20:01:10 +0000 Subject: [PATCH 12/14] fix: made some recommended changes in the filter function --- apps/web/src/hooks/useNewsletterFilters.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/src/hooks/useNewsletterFilters.ts b/apps/web/src/hooks/useNewsletterFilters.ts index d53c5866..d6db2dc3 100644 --- a/apps/web/src/hooks/useNewsletterFilters.ts +++ b/apps/web/src/hooks/useNewsletterFilters.ts @@ -55,7 +55,6 @@ export function useNewsletterFilters(initialData: Newsletter[]) { return monthYear === selectedMonth; }); - return [...filteredByMonth].sort((a, b) => { const dateA = new Date(a.time).getTime(); const dateB = new Date(b.time).getTime(); From f0ac259e6a1481e27ba626d450db5023e2e86237 Mon Sep 17 00:00:00 2001 From: Devesh Chandra Date: Mon, 17 Nov 2025 01:36:23 +0530 Subject: [PATCH 13/14] fix: minute fix changes let to const --- apps/web/src/hooks/useNewsletterFilters.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/src/hooks/useNewsletterFilters.ts b/apps/web/src/hooks/useNewsletterFilters.ts index d6db2dc3..c0fb0585 100644 --- a/apps/web/src/hooks/useNewsletterFilters.ts +++ b/apps/web/src/hooks/useNewsletterFilters.ts @@ -31,7 +31,7 @@ export function useNewsletterFilters(initialData: Newsletter[]) { // Filter and sort newsletters const filteredAndSorted = useMemo(() => { // Filter by search - let filtered = initialData.filter((item) => { + const filtered = initialData.filter((item) => { const searchLower = search.toLowerCase(); return ( item.title.toLowerCase().includes(searchLower) || @@ -57,13 +57,13 @@ export function useNewsletterFilters(initialData: Newsletter[]) { return [...filteredByMonth].sort((a, b) => { const dateA = new Date(a.time).getTime(); - const dateB = new Date(b.time).getTime(); - + const dateB = new Date(b.time).getTime(); + // Handle invalid dates if (isNaN(dateA) || isNaN(dateB)) { return 0; } - + return sortOrder === "newest" ? dateB - dateA : dateA - dateB; }); }, [initialData, search, selectedMonth, sortOrder]); @@ -90,4 +90,4 @@ export function useNewsletterFilters(initialData: Newsletter[]) { hasActiveFilters, resultCount, }; -} \ No newline at end of file +} From 2fa2f364efb341208bafa01c105bb1d38704bc20 Mon Sep 17 00:00:00 2001 From: Devesh Chandra Date: Mon, 17 Nov 2025 14:19:14 +0530 Subject: [PATCH 14/14] fix: change data to time in .md files and renamed a file --- apps/web/content/newsletters/example.md | 2 +- apps/web/content/newsletters/test-1.md | 2 +- apps/web/content/newsletters/test-2.md | 2 +- .../src/app/(main)/dashboard/newsletters/NewsletterPage.tsx | 4 ++-- apps/web/src/app/(main)/dashboard/newsletters/page.tsx | 2 +- apps/web/src/hooks/useNewsletterFilters.ts | 2 +- .../{getAllNewsLetter.ts => newsletter-metadata.ts} | 0 7 files changed, 7 insertions(+), 7 deletions(-) rename apps/web/src/utils/newsletter/{getAllNewsLetter.ts => newsletter-metadata.ts} (100%) diff --git a/apps/web/content/newsletters/example.md b/apps/web/content/newsletters/example.md index 5f724d98..119e2f96 100644 --- a/apps/web/content/newsletters/example.md +++ b/apps/web/content/newsletters/example.md @@ -1,7 +1,7 @@ --- id: "unique-newsletter-id" title: "Your Newsletter Title Goes Here" -date: "2025-11-16" +time: "2025-11-16" summary: "A brief 1-2 sentence summary of what this newsletter is about. This will appear below the title." keywords: ["keyword1", "keyword2", "keyword3"] readTime: "5 min read" diff --git a/apps/web/content/newsletters/test-1.md b/apps/web/content/newsletters/test-1.md index 90112f7f..f365c517 100644 --- a/apps/web/content/newsletters/test-1.md +++ b/apps/web/content/newsletters/test-1.md @@ -1,7 +1,7 @@ --- id: "from-farm-to-founder" title: "From Farm Boy to Founder: A Journey of Grit and Code" -date: "2025-10-23" +time: "2025-10-23" summary: "How one developer went from being a hotel receptionist with no college degree to building a startup with 10,000 users and multiple investment offers—all through self-taught programming and relentless determination." keywords: [ diff --git a/apps/web/content/newsletters/test-2.md b/apps/web/content/newsletters/test-2.md index ece9a85f..4da533fa 100644 --- a/apps/web/content/newsletters/test-2.md +++ b/apps/web/content/newsletters/test-2.md @@ -1,7 +1,7 @@ --- id: "ugly-execution" title: "Ugly Execution: Why Imperfect Action Beats Perfect Planning" -date: "2025-11-16" +time: "2025-11-16" summary: "Stop waiting for perfect conditions. Learn why taking messy, imperfect action is infinitely more valuable than endless planning and preparation." keywords: [ diff --git a/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx b/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx index 4ae2bb6c..1c9adf8e 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx @@ -4,7 +4,7 @@ import { Search } from "lucide-react"; import NewsletterItem from "@/components/newsletter/NewsletterItem"; import CustomDropdown from "@/components/newsletter/Dropdown"; import { useNewsletterFilters } from "@/hooks/useNewsletterFilters"; -import { Newsletter } from "@/utils/newsletter/getAllNewsLetter"; +import { Newsletter } from "@/utils/newsletter/newsletter-metadata"; export default function NewsletterPage({ initialData, @@ -127,4 +127,4 @@ export default function NewsletterPage({
); -} \ No newline at end of file +} diff --git a/apps/web/src/app/(main)/dashboard/newsletters/page.tsx b/apps/web/src/app/(main)/dashboard/newsletters/page.tsx index 5f73d490..22e0697f 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/page.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/page.tsx @@ -1,4 +1,4 @@ -import { getAllNewsLetter } from "@/utils/newsletter/getAllNewsLetter"; +import { getAllNewsLetter } from "@/utils/newsletter/newsletter-metadata"; import NewsletterPage from "./NewsletterPage"; export default function Page() { diff --git a/apps/web/src/hooks/useNewsletterFilters.ts b/apps/web/src/hooks/useNewsletterFilters.ts index c0fb0585..bbd315ff 100644 --- a/apps/web/src/hooks/useNewsletterFilters.ts +++ b/apps/web/src/hooks/useNewsletterFilters.ts @@ -1,5 +1,5 @@ import { useState, useMemo } from "react"; -import { Newsletter } from "@/utils/newsletter/getAllNewsLetter"; +import { Newsletter } from "@/utils/newsletter/newsletter-metadata"; type SortOrder = "newest" | "oldest"; diff --git a/apps/web/src/utils/newsletter/getAllNewsLetter.ts b/apps/web/src/utils/newsletter/newsletter-metadata.ts similarity index 100% rename from apps/web/src/utils/newsletter/getAllNewsLetter.ts rename to apps/web/src/utils/newsletter/newsletter-metadata.ts