diff --git a/apps/web/package.json b/apps/web/package.json index 77494fc3..dcd1edbf 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -27,6 +27,8 @@ "dompurify": "^3.3.0", "framer-motion": "^11.15.0", "geist": "^1.5.1", + "gray-matter": "^4.0.3", + "gsap": "^3.13.0", "lucide-react": "^0.456.0", "next": "15.5.3", "next-auth": "^4.24.11", @@ -36,6 +38,8 @@ "react-dom": "^18.2.0", "react-qr-code": "^2.0.18", "react-tweet": "^3.2.1", + "remark": "^15.0.1", + "remark-html": "^16.0.1", "superjson": "^2.2.5", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", diff --git a/apps/web/src/app/(main)/newsletters/[slug]/page.tsx b/apps/web/src/app/(main)/newsletters/[slug]/page.tsx new file mode 100644 index 00000000..a5de850c --- /dev/null +++ b/apps/web/src/app/(main)/newsletters/[slug]/page.tsx @@ -0,0 +1,86 @@ +import PageTransition from "../pagetransition"; +import Image from "next/image"; +import { ArrowLeft } from 'lucide-react'; +import { getNewsletterContent } from "@/lib/newslettercontent"; +import { posts } from "@/data/newsletters"; +import Link from "next/link"; +import { notFound } from "next/navigation"; + +export default async function NewsletterSlug({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + + const post = posts.find((p) => p.slug === slug); + + // if post doesn't exist, show 404 page + if (!post) { + notFound(); + } + + const { htmlContent } = await getNewsletterContent(slug); + + return ( + +
+ + {/* BACK BUTTON */} +
+ + + Back to Newsletters + +
+ + {/* HEADER IMAGE */} + img + + {/* ROW */} +
+

+ Article +

+ +

+ {post.date} +

+
+ + {/* CONTENT */} +
+
+
+
+
+ +
+ + ); +} \ No newline at end of file diff --git a/apps/web/src/app/(main)/newsletters/page.tsx b/apps/web/src/app/(main)/newsletters/page.tsx new file mode 100644 index 00000000..6cff28ab --- /dev/null +++ b/apps/web/src/app/(main)/newsletters/page.tsx @@ -0,0 +1,368 @@ +"use client"; + +import { useState, useMemo, useEffect } from "react"; +import Sidebar from "@/components/dashboard/Sidebar"; +import PageTransition from "./pagetransition"; +import { useShowSidebar } from "@/store/useShowSidebar"; +import { useSubscription } from "@/hooks/useSubscription"; +import Image from "next/image"; +import { posts } from "@/data/newsletters"; +import { IconWrapper } from "@/components/ui/IconWrapper"; +import { Bars3Icon } from "@heroicons/react/24/outline"; +import Link from "next/link"; +import News from "@/components/non-pro-news/News"; + +/** + * Parse a date string in "YYYY-MM-DD" format as a local date + * to avoid timezone-dependent off-by-one errors + */ +function parsePostDate(dateStr: string): Date { + const [year, month, day] = dateStr.split("-").map(Number); + return new Date(year, month - 1, day); +} + +export default function NewslettersPage() { + const { isCollapsed, showSidebar, setShowSidebar } = useShowSidebar(); + const { isPaidUser, isLoading } = useSubscription(); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedMonth, setSelectedMonth] = useState(""); + const [selectedYear, setSelectedYear] = useState(""); + + // Sidebar auto-hide hamburger when open + useEffect(() => { + if (showSidebar) { + // nothing needed, this triggers rerender & hides hamburger + } + }, [showSidebar]); + + // Get months/years from posts + const { months, years } = useMemo(() => { + const monthsSet = new Set(); + const yearsSet = new Set(); + + posts.forEach((post) => { + const date = parsePostDate(post.date); // ✅ Fixed + monthsSet.add(date.toLocaleString("default", { month: "long" })); + yearsSet.add(date.getFullYear().toString()); + }); + + return { + months: Array.from(monthsSet).sort((a, b) => { + const order = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + return order.indexOf(a) - order.indexOf(b); + }), + years: Array.from(yearsSet).sort((a, b) => Number(b) - Number(a)), + }; + }, [posts]); + + const filteredPosts = useMemo(() => { + const filtered = posts.filter((post) => { + const query = searchQuery.toLowerCase(); + const matchesSearch = post.heading.toLowerCase().includes(query); + + const date = parsePostDate(post.date); // ✅ Fixed + const month = date.toLocaleString("default", { month: "long" }); + const year = date.getFullYear().toString(); + + const matchesMonth = !selectedMonth || month === selectedMonth; + const matchesYear = !selectedYear || year === selectedYear; + + return matchesSearch && matchesMonth && matchesYear; + }); + + return [...filtered].sort( + (a, b) => parsePostDate(b.date).getTime() - parsePostDate(a.date).getTime() // ✅ Fixed + ); + }, [searchQuery, selectedMonth, selectedYear, posts]); + + const clearFilters = () => { + setSearchQuery(""); + setSelectedMonth(""); + setSelectedYear(""); + }; + + const hasActiveFilters = searchQuery || selectedMonth || selectedYear; + + // Loading + if (isLoading) { + return ( +
+ +
+
Loading...
+
+
+ ); + } + + // Non-Pro UI + if (!isPaidUser) { + return ( +
+ {/* Sidebar */} + + + {/* Overlay for mobile */} + {showSidebar && ( +
setShowSidebar(false)} + className="fixed inset-0 bg-black/40 backdrop-blur-sm xl:hidden z-40" + /> + )} + + {/* Hamburger Button */} + {!showSidebar && ( + + )} + + +
+ ); + } + + return ( +
+ {/* Sidebar */} + + + {/* Overlay (mobile only) */} + {showSidebar && ( +
setShowSidebar(false)} + className="fixed inset-0 bg-black/40 backdrop-blur-sm xl:hidden z-40" + /> + )} + + {/* Hamburger (auto-hidden when sidebar open) */} + {!showSidebar && ( + + )} + +
+ {/* Mobile header */} +
+ setShowSidebar(true)}> + + +
+ + {/* Content */} +
+ +
+ {/* LEFT Section */} +
+
+

+ + NE + +

+ +

+ + WS— + +

+ +

+ + LET + +

+ +

+ + TER + +

+ +

+ + S + +

+
+ +
+

+ Latest News +

+

+  and updates +

+
+
+ + {/* RIGHT Section */} +
+ {/* Search and Filters */} +
+ {/* Search Input */} +
+ setSearchQuery(e.target.value)} + className="w-full py-[14px] pr-[45px] pl-5 text-base border border-white/20 rounded-xl bg-white/10 backdrop-blur-[10px] text-white placeholder:text-white/60" + /> + + {searchQuery && ( + + )} +
+ + {/* Month Filter */} + + + {/* Year Filter */} + +
+ + {/* Count and Clear Filters */} + {hasActiveFilters && ( +
+

+ Found {filteredPosts.length} result + {filteredPosts.length !== 1 ? "s" : ""} +

+ +
+ )} + + {/* Posts */} + {filteredPosts.length > 0 ? ( + filteredPosts.map((post, index) => ( +
+
+ {post.heading} +
+ +
+

+ {parsePostDate(post.date).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }).toUpperCase()} +

+ +

+ {post.heading} +

+ + + Read more... + + +

+ {post.description} +

+
+
+ )) + ) : ( +
+

+ No newsletters found +

+ +
+ )} +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/(main)/newsletters/pagetransition.tsx b/apps/web/src/app/(main)/newsletters/pagetransition.tsx new file mode 100644 index 00000000..6c210cad --- /dev/null +++ b/apps/web/src/app/(main)/newsletters/pagetransition.tsx @@ -0,0 +1,124 @@ +"use client" + +import { useEffect, useRef } from "react" +import { useRouter, usePathname } from "next/navigation" +import gsap from "gsap" + +export default function PageTransition({ children }: { children: React.ReactNode }) { + const router = useRouter() + const pathname = usePathname() + + const overlayRef = useRef(null) + const blocksRef = useRef([]) + const transitioning = useRef(false) + + + const createBlocks = () => { + const overlay = overlayRef.current + if (!overlay) return + overlay.innerHTML = "" + blocksRef.current = [] + + for (let i = 0; i < 12; i++) { + const div = document.createElement("div") + div.style.flex = "1" + div.style.background = "#1e1b4b" + div.style.transform = "scaleX(0)" + div.style.transformOrigin = "left" + overlay.appendChild(div) + blocksRef.current.push(div) + } + } + + + const cover = (url: string) => { + if (transitioning.current) return + transitioning.current = true + + gsap.to(blocksRef.current, { + scaleX: 1, + duration: 0.4, + stagger: 0.07, + ease: "power2.inOut", + onComplete: () => { + router.push(url) + } + }) + } + + + const reveal = () => { + gsap.set(blocksRef.current, { + scaleX: 1, + transformOrigin: "right" + }) + + gsap.to(blocksRef.current, { + scaleX: 0, + duration: 0.4, + stagger: 0.07, + ease: "power2.inOut", + onComplete: () => { + transitioning.current = false + } + }) + } + + useEffect(() => { + createBlocks() + reveal() + + const links = document.querySelectorAll("a[href]") + const handleClick = (e: Event) => { + const target = e.currentTarget as HTMLAnchorElement + const href = target.getAttribute("href") + + // Don't hijack external links, new tabs, or modified clicks + if (!href || href.startsWith("#")) { + return + } + + // Don't hijack external URLs + if (href.startsWith("http") || href.startsWith("mailto") || href.startsWith("tel")) { + return + } + + // Don't hijack links that open in new tab or have external rel + if (target.target === "_blank" || target.rel.includes("external")) { + return + } + + // Don't hijack modified clicks (cmd/ctrl click, middle click, etc.) + const mouseEvent = e as MouseEvent + if (mouseEvent.metaKey || mouseEvent.ctrlKey || mouseEvent.shiftKey || mouseEvent.button !== 0) { + return + } + + // Only hijack internal navigation + if (href !== pathname) { + e.preventDefault() + cover(href) + } + } + + links.forEach((l) => l.addEventListener("click", handleClick)) + return () => links.forEach((l) => l.removeEventListener("click", handleClick)) + }, [pathname]) + + return ( + <> +
+ + {children} + + ) +} \ No newline at end of file diff --git a/apps/web/src/app/content/newsletters/getting-started-with-nextjs.md b/apps/web/src/app/content/newsletters/getting-started-with-nextjs.md new file mode 100644 index 00000000..c415bc25 --- /dev/null +++ b/apps/web/src/app/content/newsletters/getting-started-with-nextjs.md @@ -0,0 +1,187 @@ +# Getting Started with Next.js + +Next.js is a **powerful React framework** that enables developers to build production-ready web applications with ease. Created by Vercel, it has become one of the most popular choices for modern web development. + +## Why Next.js? + +Next.js offers several compelling features that make it stand out: + +- **Server-Side Rendering (SSR)** - Improve SEO and initial page load performance +- **File-based routing** - Intuitive routing system based on your file structure +- **API routes** - Build your backend API within the same project +- **Automatic code splitting** - Faster page loads with optimized bundles +- **Built-in CSS support** - Style your apps with CSS Modules, Sass, or Tailwind + +## Getting Started + +### Installation + +First, create a new Next.js application using the official CLI: + +```bash +npx create-next-app@latest my-next-app +cd my-next-app +npm run dev +``` + +### Project Structure + +A typical Next.js project looks like this: + +```text +my-next-app/ +├── app/ +│ ├── layout.tsx +│ ├── page.tsx +│ └── globals.css +├── public/ +│ └── images/ +├── package.json +└── next.config.js +``` + +## Core Concepts + +### 1. Pages and Routing + +Next.js uses a file-system based router. Simply create a file in the `app` directory: + +```typescript +// app/about/page.tsx +export default function About() { + return

About Page

+} +``` + +This automatically creates a route at `/about`. + + +### 3. Server vs Client Components + +**Server Components** (default): +- Fetch data on the server +- Keep sensitive data secure +- Reduce client-side JavaScript + +**Client Components** (with `'use client'`): +- Add interactivity +- Use React hooks +- Access browser APIs + +## Advanced Features + +### Image Optimization + +Next.js provides the `Image` component for automatic image optimization: + +```jsx +import Image from 'next/image' + +export default function Profile() { + return ( + Profile + ) +} +``` + +### Metadata and SEO + +Enhance your SEO with built-in metadata support: + +```typescript +export const metadata = { + title: 'My Next.js App', + description: 'A powerful web application', + openGraph: { + title: 'My Next.js App', + description: 'A powerful web application', + images: ['/og-image.jpg'], + }, +} +``` + +## Performance Tips + +> **Pro Tip**: Next.js automatically optimizes your application, but here are some best practices: + +1. **Use the `Image` component** - Automatic lazy loading and optimization +2. **Implement dynamic imports** - Load components only when needed +3. **Leverage caching** - Use appropriate cache strategies +4. **Monitor Core Web Vitals** - Keep an eye on performance metrics + +## Deployment + +Deploying to Vercel is incredibly simple: + +1. Push your code to GitHub +2. Import your repository on [Vercel](https://vercel.com) +3. Deploy with zero configuration + +### Environment Variables + +Create a `.env.local` file for local development: + +```bash +DATABASE_URL=postgresql://localhost:5432/mydb +API_KEY=your_secret_key +NEXT_PUBLIC_API_URL=https://api.example.com +``` + +> **Note**: Variables prefixed with `NEXT_PUBLIC_` are exposed to the browser. + +## Common Patterns + +### Loading States + +```typescript +export default function Loading() { + return
Loading...
+} +``` + +### Error Handling + +```typescript +'use client' + +export default function Error({ + error, + reset, +}: { + error: Error + reset: () => void +}) { + return ( +
+

Something went wrong!

+ +
+ ) +} +``` + +## Resources + +- [Official Documentation](https://nextjs.org/docs) +- [Next.js GitHub Repository](https://github.com/vercel/next.js) +- [Learn Next.js Interactive Course](https://nextjs.org/learn) +- [Next.js Examples](https://github.com/vercel/next.js/tree/canary/examples) + +--- + +## Conclusion + +Next.js combines the best of React with powerful features for production applications. Whether you're building a simple blog or a complex web application, Next.js provides the tools and performance optimizations you need. + +**Ready to start building?** Install Next.js today and experience the future of web development! + +--- + +*Last updated: November 2025* +*Written by: Harsh* \ No newline at end of file diff --git a/apps/web/src/app/content/newsletters/mastering-react-hooks.md b/apps/web/src/app/content/newsletters/mastering-react-hooks.md new file mode 100644 index 00000000..4177dec1 --- /dev/null +++ b/apps/web/src/app/content/newsletters/mastering-react-hooks.md @@ -0,0 +1,796 @@ +# Mastering React Hooks + +React Hooks revolutionized the way we write React components by allowing us to **use state and other React features in functional components**. Introduced in React 16.8, Hooks have become the standard way to build React applications. + +## Why React Hooks? + +Hooks solve several problems that existed with class components: + +- **Reusable stateful logic** - Share logic between components without HOCs or render props +- **Simpler code** - No need for class syntax, `this` keyword, or lifecycle methods +- **Better code organization** - Group related logic together instead of splitting across lifecycle methods +- **Smaller bundle size** - Functional components are more lightweight +- **Easier testing** - Pure functions are simpler to test + +## The Rules of Hooks + +> **Critical**: Always follow these two rules when using Hooks! + +1. **Only call Hooks at the top level** - Don't call Hooks inside loops, conditions, or nested functions +2. **Only call Hooks from React functions** - Call them from functional components or custom Hooks + +```typescript +// ❌ Bad - Hook inside condition +function BadComponent() { + if (condition) { + const [state, setState] = useState(0); // Wrong! + } +} + +// ✅ Good - Hook at top level +function GoodComponent() { + const [state, setState] = useState(0); + + if (condition) { + // Use the state here + } +} +``` + +## Essential Hooks + +### useState + +The most commonly used Hook for managing component state: + +```typescript +import { useState } from 'react'; + +function Counter() { + const [count, setCount] = useState(0); + const [name, setName] = useState("Alice"); + + // With objects + const [user, setUser] = useState({ + name: "Bob", + age: 30 + }); + + const incrementCount = () => { + setCount(count + 1); + // Or use functional update for correct async behavior + setCount(prev => prev + 1); + }; + + const updateUser = () => { + setUser(prev => ({ + ...prev, + age: prev.age + 1 + })); + }; + + return ( +
+

Count: {count}

+ +
+ ); +} +``` + +### useEffect + +Handle side effects like data fetching, subscriptions, or DOM manipulation: + +```typescript +import { useState, useEffect } from 'react'; + +function UserProfile({ userId }: { userId: string }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // This runs after every render (without dependency array) + console.log('Component rendered'); + }); + + useEffect(() => { + // This runs only once (empty dependency array) + console.log('Component mounted'); + + return () => { + // Cleanup function runs on unmount + console.log('Component unmounted'); + }; + }, []); + + useEffect(() => { + // This runs when userId changes + setLoading(true); + + fetch(`/api/users/${userId}`) + .then(res => res.json()) + .then(data => { + setUser(data); + setLoading(false); + }); + + // Cleanup: cancel requests on userId change + return () => { + // Cancel fetch request + }; + }, [userId]); + + if (loading) return
Loading...
; + + return
{user?.name}
; +} +``` + +### useContext + +Access context values without prop drilling: + +```typescript +import { createContext, useContext, useState } from 'react'; + +// Create context +const ThemeContext = createContext<'light' | 'dark'>('light'); + +function App() { + const [theme, setTheme] = useState<'light' | 'dark'>('light'); + + return ( + +
+
+ + ); +} + +function Header() { + const theme = useContext(ThemeContext); + + return ( +
+

My App

+
+ ); +} +``` + +## Performance Hooks + +### useMemo + +Memoize expensive calculations: + +```typescript +import { useMemo, useState } from 'react'; + +function ExpensiveComponent({ items }: { items: number[] }) { + const [count, setCount] = useState(0); + + // Without useMemo - recalculates on every render + const sum = items.reduce((a, b) => a + b, 0); + + // With useMemo - only recalculates when items change + const memoizedSum = useMemo(() => { + console.log('Calculating sum...'); + return items.reduce((a, b) => a + b, 0); + }, [items]); + + return ( +
+

Sum: {memoizedSum}

+

Count: {count}

+ +
+ ); +} +``` + +### useCallback + +Memoize functions to prevent unnecessary re-renders: + +```typescript +import { useCallback, useState, memo } from 'react'; + +// Child component wrapped in memo +const ChildComponent = memo(({ onClick }: { onClick: () => void }) => { + console.log('Child rendered'); + return ; +}); + +function ParentComponent() { + const [count, setCount] = useState(0); + const [other, setOther] = useState(0); + + // Without useCallback - new function on every render + const handleClick = () => { + console.log('Clicked!'); + }; + + // With useCallback - same function reference + const memoizedHandleClick = useCallback(() => { + console.log('Clicked!'); + setCount(prev => prev + 1); + }, []); + + return ( +
+

Count: {count}

+

Other: {other}

+ + +
+ ); +} +``` + +### useRef + +Access DOM elements or persist values without causing re-renders: + +```typescript +import { useRef, useEffect } from 'react'; + +function TextInput() { + const inputRef = useRef(null); + const renderCount = useRef(0); + + useEffect(() => { + // Auto-focus on mount + inputRef.current?.focus(); + }, []); + + useEffect(() => { + // Track renders without causing re-renders + renderCount.current += 1; + }); + + const handleClick = () => { + // Access DOM element + inputRef.current?.select(); + }; + + return ( +
+ + +

Render count: {renderCount.current}

+
+ ); +} +``` + +## Advanced Hooks + +### useReducer + +Manage complex state logic with a reducer pattern: + +```typescript +import { useReducer } from 'react'; + +type State = { + count: number; + error: string | null; +}; + +type Action = + | { type: 'increment' } + | { type: 'decrement' } + | { type: 'reset' } + | { type: 'error'; payload: string }; + +function reducer(state: State, action: Action): State { + switch (action.type) { + case 'increment': + return { ...state, count: state.count + 1, error: null }; + case 'decrement': + if (state.count === 0) { + return { ...state, error: 'Cannot go below zero' }; + } + return { ...state, count: state.count - 1, error: null }; + case 'reset': + return { count: 0, error: null }; + case 'error': + return { ...state, error: action.payload }; + default: + return state; + } +} + +function Counter() { + const [state, dispatch] = useReducer(reducer, { count: 0, error: null }); + + return ( +
+

Count: {state.count}

+ {state.error &&

{state.error}

} + + + +
+ ); +} +``` + +### useLayoutEffect + +Similar to useEffect but fires synchronously after DOM mutations: + +```typescript +import { useLayoutEffect, useRef, useState } from 'react'; + +function AnimatedComponent() { + const divRef = useRef(null); + const [height, setHeight] = useState(0); + + useLayoutEffect(() => { + // Measure DOM before browser paints + if (divRef.current) { + setHeight(divRef.current.getBoundingClientRect().height); + } + }, []); + + return ( +
+

Height: {height}px

+
+ ); +} +``` + +### useImperativeHandle + +Customize the instance value exposed to parent components: + +```typescript +import { forwardRef, useImperativeHandle, useRef } from 'react'; + +interface CustomInputHandle { + focus: () => void; + clear: () => void; +} + +const CustomInput = forwardRef((props, ref) => { + const inputRef = useRef(null); + + useImperativeHandle(ref, () => ({ + focus: () => { + inputRef.current?.focus(); + }, + clear: () => { + if (inputRef.current) { + inputRef.current.value = ''; + } + } + })); + + return ; +}); + +function Parent() { + const inputRef = useRef(null); + + return ( +
+ + + +
+ ); +} +``` + +## Custom Hooks + +Create reusable logic by extracting it into custom Hooks: + +### useLocalStorage + +```typescript +import { useState, useEffect } from 'react'; + +function useLocalStorage(key: string, initialValue: T) { + const [value, setValue] = useState(() => { + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch (error) { + console.error(error); + return initialValue; + } + }); + + useEffect(() => { + try { + window.localStorage.setItem(key, JSON.stringify(value)); + } catch (error) { + console.error(error); + } + }, [key, value]); + + return [value, setValue] as const; +} + +// Usage +function App() { + const [name, setName] = useLocalStorage('name', 'Guest'); + + return ( + setName(e.target.value)} + /> + ); +} +``` + +### useFetch + +```typescript +import { useState, useEffect } from 'react'; + +interface FetchState { + data: T | null; + loading: boolean; + error: Error | null; +} + +function useFetch(url: string) { + const [state, setState] = useState>({ + data: null, + loading: true, + error: null + }); + + useEffect(() => { + let mounted = true; + + const fetchData = async () => { + try { + setState(prev => ({ ...prev, loading: true })); + const response = await fetch(url); + const data = await response.json(); + + if (mounted) { + setState({ data, loading: false, error: null }); + } + } catch (error) { + if (mounted) { + setState({ data: null, loading: false, error: error as Error }); + } + } + }; + + fetchData(); + + return () => { + mounted = false; + }; + }, [url]); + + return state; +} + +// Usage +function UserList() { + const { data, loading, error } = useFetch('/api/users'); + + if (loading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return ( +
    + {data?.map(user => ( +
  • {user.name}
  • + ))} +
+ ); +} +``` + +### useDebounce + +```typescript +import { useState, useEffect } from 'react'; + +function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} + +// Usage +function SearchComponent() { + const [searchTerm, setSearchTerm] = useState(''); + const debouncedSearchTerm = useDebounce(searchTerm, 500); + + useEffect(() => { + if (debouncedSearchTerm) { + // Make API call + console.log('Searching for:', debouncedSearchTerm); + } + }, [debouncedSearchTerm]); + + return ( + setSearchTerm(e.target.value)} + placeholder="Search..." + /> + ); +} +``` + +### useToggle + +```typescript +import { useState, useCallback } from 'react'; + +function useToggle(initialValue: boolean = false) { + const [value, setValue] = useState(initialValue); + + const toggle = useCallback(() => { + setValue(v => !v); + }, []); + + return [value, toggle] as const; +} + +// Usage +function Modal() { + const [isOpen, toggleOpen] = useToggle(false); + + return ( +
+ + {isOpen &&
Modal Content
} +
+ ); +} +``` + +## Common Patterns + +### Fetching Data on Mount + +```typescript +function UserProfile({ userId }: { userId: string }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + + async function fetchUser() { + try { + const response = await fetch(`/api/users/${userId}`); + const data = await response.json(); + + if (!cancelled) { + setUser(data); + setLoading(false); + } + } catch (error) { + if (!cancelled) { + console.error(error); + setLoading(false); + } + } + } + + fetchUser(); + + return () => { + cancelled = true; + }; + }, [userId]); + + if (loading) return
Loading...
; + return
{user?.name}
; +} +``` + +### Form Handling + +```typescript +function LoginForm() { + const [formData, setFormData] = useState({ + email: '', + password: '' + }); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + console.log('Submitting:', formData); + }; + + return ( +
+ + + +
+ ); +} +``` + +### Previous Value Tracking + +```typescript +function usePrevious(value: T): T | undefined { + const ref = useRef(); + + useEffect(() => { + ref.current = value; + }, [value]); + + return ref.current; +} + +// Usage +function Counter() { + const [count, setCount] = useState(0); + const prevCount = usePrevious(count); + + return ( +
+

Current: {count}

+

Previous: {prevCount}

+ +
+ ); +} +``` + +## Best Practices + +| Practice | Description | Example | +|----------|-------------|---------| +| **Dependency arrays** | Always include all dependencies | `useEffect(() => {...}, [dep1, dep2])` | +| **Cleanup functions** | Return cleanup from useEffect | `return () => clearInterval(id)` | +| **Functional updates** | Use functions for state updates | `setState(prev => prev + 1)` | +| **Extract custom hooks** | Reuse logic across components | `const data = useFetch(url)` | +| **Avoid inline objects** | Memoize objects in dependencies | `useMemo(() => ({...}), [])` | + +> **Pro Tip**: Use the ESLint plugin `eslint-plugin-react-hooks` to catch Hook mistakes automatically! + +## Common Mistakes + +### 1. Missing Dependencies + +```typescript +// ❌ Bad - missing dependency +useEffect(() => { + console.log(count); +}, []); + +// ✅ Good - include all dependencies +useEffect(() => { + console.log(count); +}, [count]); +``` + +### 2. Stale Closures + +```typescript +// ❌ Bad - stale closure +useEffect(() => { + const interval = setInterval(() => { + setCount(count + 1); // Uses stale count + }, 1000); + return () => clearInterval(interval); +}, []); + +// ✅ Good - functional update +useEffect(() => { + const interval = setInterval(() => { + setCount(prev => prev + 1); // Always fresh + }, 1000); + return () => clearInterval(interval); +}, []); +``` + +### 3. Unnecessary Dependencies + +```typescript +// ❌ Bad - object recreated on every render +const options = { method: 'GET' }; + +useEffect(() => { + fetch(url, options); +}, [url, options]); // options changes every render! + +// ✅ Good - memoize the object +const options = useMemo(() => ({ method: 'GET' }), []); + +useEffect(() => { + fetch(url, options); +}, [url, options]); +``` + +## Testing Hooks + +```typescript +import { renderHook, act } from '@testing-library/react'; +import { useState } from 'react'; + +function useCounter(initialValue = 0) { + const [count, setCount] = useState(initialValue); + const increment = () => setCount(c => c + 1); + return { count, increment }; +} + +test('useCounter increments count', () => { + const { result } = renderHook(() => useCounter(0)); + + expect(result.current.count).toBe(0); + + act(() => { + result.current.increment(); + }); + + expect(result.current.count).toBe(1); +}); +``` + +## Resources + +- [React Hooks Documentation](https://react.dev/reference/react) +- [Hooks API Reference](https://react.dev/reference/react/hooks) +- [React Hooks FAQ](https://react.dev/learn#adding-interactivity) +- [useHooks.com](https://usehooks.com/) - Collection of custom Hooks +- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) + +--- + +## Conclusion + +React Hooks have transformed how we build React applications, making code more reusable, testable, and maintainable. By mastering the built-in Hooks and creating custom ones, you can write cleaner, more efficient React code. + +**Start using Hooks today** and experience the power of functional components with state and side effects! + +--- + +*Last updated: November 2025* +*Written by: Harsh* \ No newline at end of file diff --git a/apps/web/src/app/content/newsletters/understanding-typescript.md b/apps/web/src/app/content/newsletters/understanding-typescript.md new file mode 100644 index 00000000..37cd7112 --- /dev/null +++ b/apps/web/src/app/content/newsletters/understanding-typescript.md @@ -0,0 +1,513 @@ +# Understanding TypeScript + +TypeScript is a **statically typed superset of JavaScript** that compiles to plain JavaScript. Developed and maintained by Microsoft, it has become the go-to choice for building robust, scalable applications. + +## Why TypeScript? + +TypeScript addresses JavaScript's limitations and brings several powerful features: + +- **Static type checking** - Catch errors before runtime +- **Enhanced IDE support** - Better autocomplete and refactoring +- **Modern JavaScript features** - Use ES6+ features with backward compatibility +- **Better code documentation** - Types serve as inline documentation +- **Improved maintainability** - Easier to refactor large codebases + +## Getting Started + +### Installation + +Install TypeScript globally or in your project: + +```bash +# Global installation +npm install -g typescript + +# Project installation +npm install --save-dev typescript + +# Initialize TypeScript config +tsc --init +``` + +### Your First TypeScript File + +Create a `hello.ts` file: + +```typescript +function greet(name: string): string { + return `Hello, ${name}!`; +} + +const message = greet("TypeScript"); +console.log(message); +``` + +Compile and run: + +```bash +tsc hello.ts +node hello.js +``` + +### Type Annotations + +```typescript +// Variables +let username: string = "Alice"; +let age: number = 30; +let isActive: boolean = true; + +// Arrays +let numbers: number[] = [1, 2, 3]; +let strings: Array = ["a", "b", "c"]; + +// Objects +let user: { name: string; age: number } = { + name: "Bob", + age: 25 +}; + +// Functions +function add(a: number, b: number): number { + return a + b; +} + +const multiply = (a: number, b: number): number => a * b; +``` + +## Interfaces and Types + +### Interfaces + +Interfaces define the structure of objects: + +```typescript +interface User { + id: number; + name: string; + email: string; + age?: number; // Optional property + readonly createdAt: Date; // Read-only +} + +const user: User = { + id: 1, + name: "Alice", + email: "alice@example.com", + createdAt: new Date() +}; + +// Extending interfaces +interface Admin extends User { + role: string; + permissions: string[]; +} +``` + +### Type Aliases + +Type aliases create custom types: + +```typescript +type ID = string | number; +type Status = "pending" | "approved" | "rejected"; + +type Product = { + id: ID; + name: string; + price: number; + status: Status; +}; + +// Union types +type Result = Success | Error; + +// Intersection types +type Employee = Person & { employeeId: number }; +``` + +## Advanced Types + +### Generics + +Generics create reusable, type-safe components: + +```typescript +// Generic function +function identity(arg: T): T { + return arg; +} + +const num = identity(42); +const str = identity("hello"); + +// Generic interface +interface ApiResponse { + data: T; + status: number; + message: string; +} + +// Generic class +class DataStore { + private data: T[] = []; + + add(item: T): void { + this.data.push(item); + } + + get(index: number): T { + return this.data[index]; + } +} +``` + +### Union and Intersection Types + +```typescript +// Union - can be one of several types +type StringOrNumber = string | number; + +function format(value: StringOrNumber): string { + if (typeof value === "string") { + return value.toUpperCase(); + } + return value.toFixed(2); +} + +// Intersection - combines multiple types +type Named = { name: string }; +type Aged = { age: number }; +type Person = Named & Aged; + +const person: Person = { + name: "Alice", + age: 30 +}; +``` + +### Utility Types + +TypeScript provides built-in utility types: + +```typescript +interface Todo { + title: string; + description: string; + completed: boolean; +} + +// Partial - makes all properties optional +type PartialTodo = Partial; + +// Required - makes all properties required +type RequiredTodo = Required; + +// Readonly - makes all properties readonly +type ReadonlyTodo = Readonly; + +// Pick - selects specific properties +type TodoPreview = Pick; + +// Omit - removes specific properties +type TodoInfo = Omit; + +// Record - creates object type with specific keys +type UserRoles = Record; +``` + +## Classes and OOP + +TypeScript enhances classes with type safety: + +```typescript +class Animal { + protected name: string; + + constructor(name: string) { + this.name = name; + } + + public move(distance: number): void { + console.log(`${this.name} moved ${distance}m`); + } +} + +class Dog extends Animal { + private breed: string; + + constructor(name: string, breed: string) { + super(name); + this.breed = breed; + } + + bark(): void { + console.log("Woof! Woof!"); + } +} + +// Abstract classes +abstract class Shape { + abstract area(): number; + + describe(): void { + console.log(`Area: ${this.area()}`); + } +} + +class Circle extends Shape { + constructor(private radius: number) { + super(); + } + + area(): number { + return Math.PI * this.radius ** 2; + } +} +``` + +## Type Guards and Narrowing + +> **Pro Tip**: Type guards help TypeScript understand types in conditional blocks. + +```typescript +// typeof guard +function process(value: string | number) { + if (typeof value === "string") { + return value.toUpperCase(); // TypeScript knows it's a string + } + return value.toFixed(2); // TypeScript knows it's a number +} + +// instanceof guard +class Cat { meow() {} } +class Dog { bark() {} } + +function makeSound(animal: Cat | Dog) { + if (animal instanceof Cat) { + animal.meow(); + } else { + animal.bark(); + } +} + +// Custom type guard +interface Fish { swim(): void; } +interface Bird { fly(): void; } + +function isFish(pet: Fish | Bird): pet is Fish { + return (pet as Fish).swim !== undefined; +} +``` + +## Enums + +Enums define a set of named constants: + +```typescript +// Numeric enum +enum Direction { + Up = 1, + Down, + Left, + Right +} + +// String enum +enum Status { + Active = "ACTIVE", + Inactive = "INACTIVE", + Pending = "PENDING" +} + +// Using enums +let currentDirection: Direction = Direction.Up; +let userStatus: Status = Status.Active; + +// Const enum (optimized) +const enum Colors { + Red, + Green, + Blue +} +``` + +## Modules and Namespaces + +### ES6 Modules + +```typescript +// math.ts +export function add(a: number, b: number): number { + return a + b; +} + +export const PI = 3.14159; + +// app.ts +import { add, PI } from "./math"; + +console.log(add(2, 3)); +console.log(PI); +``` + +### Namespaces + +```typescript +namespace Validation { + export interface StringValidator { + isValid(s: string): boolean; + } + + export class EmailValidator implements StringValidator { + isValid(email: string): boolean { + return email.includes("@"); + } + } +} + +const validator = new Validation.EmailValidator(); +``` + +## Configuration + +### tsconfig.json + +Key compiler options: + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.spec.ts"] +} +``` + +## Best Practices + +1. **Enable strict mode** - Use `"strict": true` in tsconfig.json +2. **Avoid `any`** - Use `unknown` or specific types instead +3. **Use interfaces for objects** - More extensible than type aliases +4. **Leverage type inference** - Let TypeScript infer types when possible +5. **Write small, focused types** - Easier to maintain and reuse + +```typescript +// ❌ Bad +function process(data: any) { + return data.value; +} + +// ✅ Good +interface Data { + value: string; +} + +function process(data: Data): string { + return data.value; +} +``` + +## Common Patterns + +### Discriminated Unions +```typescript +interface Success { + type: "success"; + data: T; +} + +interface Failure { + type: "failure"; + error: string; +} + +type Result = Success | Failure; + +function handleResult(result: Result) { + if (result.type === "success") { + console.log(result.data); + } else { + console.error(result.error); + } +} +``` + +### Mapped Types + +```typescript +type Readonly = { + readonly [P in keyof T]: T[P]; +}; + +type Optional = { + [P in keyof T]?: T[P]; +}; + +interface User { + name: string; + age: number; +} + +type ReadonlyUser = Readonly; +type OptionalUser = Optional; +``` + +## TypeScript with React + +```typescript +import React, { useState } from 'react'; + +interface Props { + name: string; + age?: number; + onClick: (id: number) => void; +} + +const UserCard: React.FC = ({ name, age, onClick }) => { + const [count, setCount] = useState(0); + + return ( +
onClick(count)}> +

{name}

+ {age &&

Age: {age}

} + +
+ ); +}; +``` + +## Resources + +- [Official TypeScript Documentation](https://www.typescriptlang.org/docs/) +- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) +- [TypeScript Playground](https://www.typescriptlang.org/play) +- [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) - Type definitions for JavaScript libraries +- [TypeScript Deep Dive](https://basarat.gitbook.io/typescript/) + +--- + +## Conclusion + +TypeScript transforms JavaScript development by adding type safety, better tooling, and enhanced code quality. While there's a learning curve, the benefits of catching errors early and having better IDE support make it worthwhile for projects of any size. + +**Ready to level up your JavaScript?** Start using TypeScript today and experience the difference! + +--- + +*Last updated: November 2025* +*Written by: Harsh* \ No newline at end of file diff --git a/apps/web/src/app/docs/how-to-add-newsletters.md b/apps/web/src/app/docs/how-to-add-newsletters.md new file mode 100644 index 00000000..9bdf1be8 --- /dev/null +++ b/apps/web/src/app/docs/how-to-add-newsletters.md @@ -0,0 +1,124 @@ +# 📄 How to Add a New Newsletter +This guide explains how to add a new newsletter to the Opensox platform using the **updated newsletter data structure**, where: + +- **Metadata** → lives in `apps/web/src/data/newsletters.ts` +- **Content (Markdown)** → lives in + `apps/web/src/app/content/newsletters/[slug].md` + +--- + +## 🗂 Folder Structure + +\`\`\` +apps/web/src/ +├── app/ +│ └── content/ +│ └── newsletters/ +│ ├── getting-started-with-nextjs.md +│ ├── understanding-typescript.md +│ └── mastering-react-hooks.md +└── data/ + └── newsletters.ts +\`\`\` + +--- + +## ✅ Step 1: Add Your Markdown Content + +Create a new file inside: + +\`\`\` +apps/web/src/app/content/newsletters/ +\`\`\` + +Example: + +\`\`\` +my-awesome-guide.md +\`\`\` + +Inside it, write your Markdown content: + +\`\`\`md +# My Awesome Guide + +Here is the content of my newsletter... + +- Introduction +- Key points +- Summary + +Happy reading! +\`\`\` + +--- + +## ✅ Step 2: Add Metadata in `newsletters.ts` + +Open: + +\`\`\` +apps/web/src/data/newsletters.ts +\`\`\` + +Add a new object inside the `posts` array. + +Example: + +\`\`\`ts +import photo from '../assets/images/photu.jpg'; +import coverimg from '../assets/images/gsoc.png'; +import { StaticImageData } from 'next/image'; + +export interface Post { + date: string; + heading: string; + description: string; + image: StaticImageData; + coverimg: StaticImageData; + slug: string; +} + +export const posts: Post[] = [ + { + date: "2024-05-01", + heading: "My Awesome Guide", + description: + "A walkthrough of an important topic, broken down into simple concepts and examples.", + image: photo, + coverimg: coverimg, + slug: "my-awesome-guide", + }, +]; +\`\`\` + +### 🔥 Important: + +- `slug` **must match the markdown filename** + Example: + `slug: "my-awesome-guide"` → file must be: + `my-awesome-guide.md` +- `image` → small card image +- `coverimg` → large header image +- `date` → `"YYYY-MM-DD"` format + +--- + +## ✅ Step 3: Add Images (If Needed) + +Place images in: + +\`\`\` +apps/web/src/assets/images/ +\`\`\` + +Then import in `newsletters.ts`: + +\`\`\`ts +import img from '../assets/images/myimage.webp'; +\`\`\` + +--- + +## 🎉 Done! +You've successfully added a new newsletter using the updated structure. diff --git a/apps/web/src/assets/images/gsoc.png b/apps/web/src/assets/images/gsoc.png new file mode 100644 index 00000000..8f546eda Binary files /dev/null and b/apps/web/src/assets/images/gsoc.png differ diff --git a/apps/web/src/assets/images/nextjs.webp b/apps/web/src/assets/images/nextjs.webp new file mode 100644 index 00000000..4e7fef16 Binary files /dev/null and b/apps/web/src/assets/images/nextjs.webp differ diff --git a/apps/web/src/assets/images/opensox.jpg b/apps/web/src/assets/images/opensox.jpg new file mode 100644 index 00000000..1a182920 Binary files /dev/null and b/apps/web/src/assets/images/opensox.jpg differ diff --git a/apps/web/src/assets/images/photu.jpg b/apps/web/src/assets/images/photu.jpg new file mode 100644 index 00000000..249fe3e7 Binary files /dev/null and b/apps/web/src/assets/images/photu.jpg differ diff --git a/apps/web/src/components/dashboard/Sidebar.tsx b/apps/web/src/components/dashboard/Sidebar.tsx index fef667ed..aa96dfa7 100644 --- a/apps/web/src/components/dashboard/Sidebar.tsx +++ b/apps/web/src/components/dashboard/Sidebar.tsx @@ -6,22 +6,25 @@ import SidebarItem from "../sidebar/SidebarItem"; import { useRouter } from "next/navigation"; import { IconWrapper } from "../ui/IconWrapper"; import { motion, AnimatePresence } from "framer-motion"; + import { XMarkIcon, HomeIcon, FolderIcon, - ArrowRightOnRectangleIcon, SparklesIcon, StarIcon, DocumentTextIcon, Cog6ToothIcon, + NewspaperIcon, + ArrowRightOnRectangleIcon, } from "@heroicons/react/24/outline"; + import { useShowSidebar } from "@/store/useShowSidebar"; import { signOut, useSession } from "next-auth/react"; import { ProfilePic } from "./ProfilePic"; import { useSubscription } from "@/hooks/useSubscription"; import { OpensoxProBadge } from "../sheet/OpensoxProBadge"; -import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; +import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; const SIDEBAR_ROUTES = [ { @@ -34,6 +37,11 @@ const SIDEBAR_ROUTES = [ label: "OSS Projects", icon: , }, + { + path: "/newsletters", + label: "Newsletters", + icon: , + }, { path: "/dashboard/sheet", label: "OSS Sheet", @@ -51,12 +59,9 @@ export default function Sidebar({ overlay = false }: { overlay?: boolean }) { }; const proClickHandler = () => { - if (isPaidUser) { - router.push("/dashboard/pro/dashboard"); - } else { - router.push("/pricing"); - } + router.push(isPaidUser ? "/dashboard/pro/dashboard" : "/pricing"); }; + const desktopWidth = isCollapsed ? 80 : 288; const mobileWidth = desktopWidth; @@ -72,26 +77,24 @@ export default function Sidebar({ overlay = false }: { overlay?: boolean }) { style={{ width: overlay ? mobileWidth : desktopWidth }} > {/* Mobile header */} -
-
- - Opensox AI - -
+
+ + Opensox AI + setShowSidebar(false)}>
- {/* Desktop header with collapse */} + {/* Desktop header */}
{!isCollapsed && ( Opensox AI @@ -108,30 +111,27 @@ export default function Sidebar({ overlay = false }: { overlay?: boolean }) {
-
- {SIDEBAR_ROUTES.map((route) => { - return ( - - - - ); - })} + {/* Sidebar body */} +
+ {SIDEBAR_ROUTES.map((route) => ( + + + + ))} + } collapsed={isCollapsed} /> + {!isCollapsed && !isPaidUser ? (
- +
@@ -146,12 +146,11 @@ export default function Sidebar({ overlay = false }: { overlay?: boolean }) { itemName="Opensox Pro" onclick={proClickHandler} icon={} - collapsed={isCollapsed} - /> + collapsed={isCollapsed} + /> )}
- {/* Bottom profile */} ); @@ -163,74 +162,63 @@ function ProfileMenu({ isCollapsed }: { isCollapsed: boolean }) { const router = useRouter(); const fullName = session?.user?.name || "User"; - const firstName = fullName.split(" ")[0]; const userEmail = session?.user?.email || ""; const userImage = session?.user?.image || null; - // Close dropdown when clicking outside React.useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - const target = event.target as HTMLElement; - if (open && !target.closest(".profile-menu-container")) { + const handler = (e: MouseEvent) => { + if (open && !(e.target as HTMLElement).closest(".profile-menu-container")) { setOpen(false); } }; - - if (open) { - document.addEventListener("mousedown", handleClickOutside); - return () => - document.removeEventListener("mousedown", handleClickOutside); - } + if (open) document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); }, [open]); return (
setOpen((s) => !s)} + onClick={() => setOpen((o) => !o)} > {!isCollapsed && (
- - {firstName} - + {fullName} {userEmail}
)}
- {/* Profile Card Dropdown */} + {!isCollapsed && open && ( - - {/* User Info Section */}
- - {fullName} - + {fullName} {userEmail}
- {/* Menu Items */}
+
); -} +} \ No newline at end of file diff --git a/apps/web/src/components/non-pro-news/News.tsx b/apps/web/src/components/non-pro-news/News.tsx new file mode 100644 index 00000000..eab950f5 --- /dev/null +++ b/apps/web/src/components/non-pro-news/News.tsx @@ -0,0 +1,42 @@ +import { Terminal } from "lucide-react"; +import PrimaryButton from "../ui/custom-button"; +import Link from "next/link"; + +export default function News() { + return ( +
+
+
+
+ +
+
+

+ Stay Ahead in Open +

+

+ Source—with Opensox +

+
+ + {/* Subheading */} +
+

+ Curated insights, repo recommendations, industry news and developer tools. +

+

+ No spam. Just high-quality updates that save you hours. +

+
+ + {/* CTA Button */} + + + + Subscribe Now + + +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/data/newsletters.ts b/apps/web/src/data/newsletters.ts new file mode 100644 index 00000000..1625e499 --- /dev/null +++ b/apps/web/src/data/newsletters.ts @@ -0,0 +1,45 @@ +import photo from '../assets/images/photu.jpg'; +import user from '../assets/images/user_dp.webp'; +import coverimg1 from '../assets/images/gsoc.png'; +import coverimg2 from '../assets/images/opensox.jpg' +import { StaticImageData } from 'next/image'; + +export interface Post { + date: string; + heading: string; + description: string; + image: StaticImageData; + coverimg:StaticImageData, + slug: string; +} + +export const posts: Post[] = [ + { + date: "2024-03-15", + heading: "Getting Started with Next.js", + description: + "Next.js is a powerful React framework that enables server-side rendering and static site generation. It provides an excellent developer experience with features like file-based routing, API routes, and automatic code splitting.", + image: photo, + coverimg: coverimg1, + slug: "getting-started-with-nextjs", + }, + { + date: "2024-03-10", + heading: "Understanding TypeScript", + description: + "TypeScript adds static typing to JavaScript, helping catch errors early in development. It improves code quality, maintainability, and provides better IDE support with autocomplete and inline documentation.", + image: user, + coverimg: coverimg2, + slug: "understanding-typescript", + }, + + { + date: "2024-02-20", + heading: "Mastering React Hooks", + description: + "React Hooks simplify state management and side effects in React applications. Learn how useState, useEffect, and custom hooks can make your components cleaner and more powerful.", + image: photo, + coverimg: coverimg1, + slug: "mastering-react-hooks", + }, +]; diff --git a/apps/web/src/lib/newslettercontent.ts b/apps/web/src/lib/newslettercontent.ts new file mode 100644 index 00000000..9d7dfd82 --- /dev/null +++ b/apps/web/src/lib/newslettercontent.ts @@ -0,0 +1,45 @@ +// src/lib/newslettercontent.ts +import fs from "fs/promises"; +import path from "path"; +import matter from "gray-matter"; +import { remark } from "remark"; +import html from "remark-html"; + +export async function getNewsletterContent(slug: string) { + try { + // ✅ FIXED: Validate slug to prevent path traversal attacks + if (!slug || slug.includes("..") || slug.includes("/") || slug.includes("\\")) { + throw new Error("Invalid slug"); + } + + const filePath = path.join( + process.cwd(), + "src/app/content/newsletters", + `${slug}.md` + ); + + // ✅ FIXED: Use async file reading and add error handling + let fileContent: string; + try { + fileContent = await fs.readFile(filePath, "utf-8"); + } catch (error) { + throw new Error(`Newsletter not found: ${slug}`); + } + + // Parse markdown front matter + const { content, data } = matter(fileContent); + + // Convert markdown to HTML + const processed = await remark().use(html).process(content); + const htmlContent = processed.toString(); + + return { + htmlContent, + meta: data || {}, + }; + } catch (error) { + // ✅ FIXED: Add error handling as requested by CodeRabbit + console.error(`Error loading newsletter ${slug}:`, error); + throw error; + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6fa2dbc..f5ad9893 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -181,6 +181,12 @@ 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 + gsap: + specifier: ^3.13.0 + version: 3.13.0 lucide-react: specifier: ^0.456.0 version: 0.456.0(react@18.3.1) @@ -208,6 +214,12 @@ 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-html: + specifier: ^16.0.1 + version: 16.0.1 superjson: specifier: ^2.2.5 version: 2.2.5 @@ -1285,6 +1297,9 @@ packages: '@types/cors@2.8.19': resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@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. @@ -1304,6 +1319,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 +1337,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==} @@ -1375,6 +1396,9 @@ packages: '@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==} + '@typescript-eslint/eslint-plugin@6.21.0': resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1940,6 +1964,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 +2057,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 +2075,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 +2156,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 +2272,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 +2341,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==} @@ -2672,6 +2720,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 +2967,13 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + + gsap@3.13.0: + resolution: {integrity: sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==} + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -2948,6 +3010,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 +3033,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 +3158,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 +3367,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 +3436,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 @@ -3393,6 +3478,21 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + 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 +3511,69 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + 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'} @@ -3886,6 +4049,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 +4157,18 @@ packages: resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} hasBin: true + 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 +4251,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 +4370,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 +4440,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 +4451,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 +4588,12 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -4544,6 +4742,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 +4822,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 +4946,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': {} @@ -5605,6 +5830,10 @@ snapshots: dependencies: '@types/node': 20.19.0 + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + '@types/dompurify@3.2.0': dependencies: dompurify: 3.3.0 @@ -5635,6 +5864,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 +5884,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': {} @@ -5706,6 +5943,8 @@ snapshots: '@types/trusted-types@2.0.7': optional: true + '@types/unist@3.0.3': {} + '@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: '@eslint-community/regexpp': 4.12.1 @@ -6348,6 +6587,8 @@ snapshots: axobject-query@4.1.0: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -6448,6 +6689,8 @@ snapshots: caniuse-lite@1.0.30001731: {} + ccount@2.0.1: {} + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -6485,6 +6728,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 +6808,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + commander@10.0.1: {} commander@4.1.1: {} @@ -6651,6 +6902,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 +6990,10 @@ snapshots: detect-newline@4.0.1: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + didyoumean@1.2.2: {} diff@4.0.2: {} @@ -7421,6 +7680,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 +7969,15 @@ 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 + + gsap@3.13.0: {} + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -7737,6 +8011,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 +8048,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 +8206,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 +8406,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 +8460,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 @@ -8190,6 +8496,56 @@ snapshots: math-intrinsics@1.1.0: {} + 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-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 +8556,139 @@ 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-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 @@ -8681,6 +9170,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 +9312,38 @@ snapshots: dependencies: jsesc: 0.5.0 + 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 +9428,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 +9607,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 +9704,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 +9717,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 +9854,10 @@ snapshots: tr46@0.0.3: {} + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@1.4.3(typescript@5.9.2): dependencies: typescript: 5.9.2 @@ -9463,6 +10004,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 +10109,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 +10244,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: {}