diff --git a/README.md b/README.md index 4d509de..9ea5a00 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Blog Kit -A powerful Next.js-first toolkit for building professional blog systems with markdown. Consists of two packages: a core library for server-side markdown parsing and a React component library for rendering beautiful blog UIs. +A powerful Next.js-first toolkit for building professional blog systems with markdown. Consists of +two packages: a core library for server-side markdown parsing and a React component library for +rendering beautiful blog UIs. ## Documentation diff --git a/app/content/docs/api-reference.md b/app/content/docs/api-reference.md index a1c2317..b3e6949 100644 --- a/app/content/docs/api-reference.md +++ b/app/content/docs/api-reference.md @@ -1,88 +1,204 @@ --- -title: Blog Kit API Reference -description: Complete API reference for Blog Kit's core package and React components. +title: API Reference +description: + Complete API reference for all Blog Kit functions, components, hooks, utility functions, and + TypeScript types. date: 2024-01-06 --- ## API Reference -Complete reference documentation for all functions, components, hooks, and types available in Blog -Kit. This guide covers both the core package utilities and React components with detailed -parameters, return values, and usage examples. +The complete reference for everything exported by Blog Kit. For tutorials and usage examples, see +[Getting Started](/docs/getting-started) and [Guides](/docs/guides). -### Core Package +--- + +### Core Package (`@haroonwaves/blog-kit-core`) -#### Server-Side Functions (SSR/SSG only) +All core functions require Node.js (`fs` module) and only work in server environments (Next.js +SSR/SSG, Node.js scripts, etc.). -##### `getAllBlogsMeta(config: BlogConfig): BlogMeta[]` +#### `getAllBlogsMeta(config: BlogConfig): BlogMeta[]` -Returns an array of all blog metadata sorted by date (newest first). Requires Node.js `fs` module. +Returns an array of all blog metadata sorted by date (newest first). **Parameters:** - `config.contentDirectory` (string): Path to your content directory -- `config.blogSubdirectory` (string, optional): Subdirectory for blog files (defaults to 'blog') +- `config.blogSubdirectory` (string, optional): Subdirectory for blog files (defaults to `'blog'`) **Returns:** Array of `BlogMeta` objects -**Example:** - ```typescript const blogsMeta = getAllBlogsMeta({ - contentDirectory: './content', - blogSubdirectory: 'posts', // optional + contentDirectory: process.cwd(), + blogSubdirectory: 'content/blog', }); -// blogs is an array of BlogMeta objects blogsMeta.forEach((meta) => { console.log(meta.title, meta.slug, meta.readingTime); }); ``` -##### `getBlog(slug: string, config: BlogConfig): Blog | null` +#### `getBlog(slug: string, config: BlogConfig): Blog | null` -Returns the full blog data including content for a specific slug. Requires Node.js `fs` module. +Returns the full blog data (metadata + content) for a specific slug. **Parameters:** -- `slug` (string): The blog post slug (filename without .md) +- `slug` (string): The blog post slug (filename without `.md`) - `config.contentDirectory` (string): Path to your content directory -- `config.blogSubdirectory` (string, optional): Subdirectory for blog files (defaults to 'blog') +- `config.blogSubdirectory` (string, optional): Subdirectory for blog files (defaults to `'blog'`) **Returns:** `Blog` object or `null` if not found -**Example:** - ```typescript const blog = getBlog('my-blog-post', { - contentDirectory: './content', + contentDirectory: process.cwd(), + blogSubdirectory: 'content/blog', }); if (blog) { console.log(blog.metadata.title); - console.log(blog.content); // blog markdown content - console.log(blog.readingTime); + console.log(blog.metadata.readingTime); + console.log(blog.content); } ``` -### React Package +--- + +### React Package (`@haroonwaves/blog-kit-react`) #### Components -- `BlogRenderer` - Renders markdown content with syntax highlighting -- `BlogCard` - Single blog post card component -- `BlogList` - List of blog cards -- `BlogPlaceholder` - Loading placeholder component +##### `BlogList` + +A stateless component that renders a list of blog cards. Works with both server-side and client-side +rendering. + +| Prop | Type | Default | Description | +| -------------- | --------------------------------------------------------------- | ------------------------ | ------------------------------------------ | +| `metadata` | `BlogMeta[]` | _required_ | Array of blog metadata to render | +| `title` | `string` | — | Page title (rendered as h1) | +| `description` | `string` | — | Page description | +| `basePath` | `string` | `'/blog'` | Base URL for blog post links | +| `renderLink` | `(href, children) => ReactNode` | — | Custom link renderer (e.g. Next.js `Link`) | +| `className` | `string` | — | CSS class for the card container | +| `emptyMessage` | `string` | `'No blog posts found.'` | Message when list is empty | +| `cardProps` | `Omit` | — | Props forwarded to each `BlogCard` | +| `classNames` | `{ title?, description? }` | — | CSS overrides for title and description | + +--- + +##### `BlogCard` + +Renders a single blog post card with category badges, reading time, and date. + +| Prop | Type | Default | Description | +| ----------------- | ------------------------------- | ---------- | ------------------------------------------ | +| `metadata` | `BlogMeta` | _required_ | Blog metadata object | +| `basePath` | `string` | `'/blog'` | Base path for blog links | +| `renderLink` | `(href, children) => ReactNode` | — | Custom link renderer (e.g. Next.js `Link`) | +| `className` | `string` | — | Additional CSS classes | +| `showCategory` | `boolean` | `true` | Show category badge | +| `showReadingTime` | `boolean` | `true` | Show reading time | +| `showDate` | `boolean` | `true` | Show publication date | + +--- + +##### `BlogRenderer` + +Renders markdown content with syntax highlighting (Prism), GFM support, heading anchor links, and +styled typography. Supports custom component overrides. + +| Prop | Type | Default | Description | +| ----------------- | ------------------------------- | ---------- | --------------------------------------- | +| `content` | `string` | _required_ | Markdown content to render | +| `metadata` | `BlogMeta` | _required_ | Blog metadata object | +| `className` | `string` | — | Additional CSS classes | +| `components` | `Record` | — | Override default HTML element renderers | +| `showCategory` | `boolean` | `true` | Show category badge | +| `showReadingTime` | `boolean` | `true` | Show reading time | +| `showDate` | `boolean` | `true` | Show publication date | + +**Overridable elements:** `h1`–`h6`, `p`, `a`, `blockquote`, `code`, `pre`, `img`, `table`, `thead`, +`tbody`, `tr`, `th`, `td`, `ul`, `ol`, `li`, `strong`, `em`, `del`, `hr`, `br`, `input` + +--- + +##### `Filter` + +A client component for searching and category filtering. + +| Prop | Type | Default | Description | +| --------------------- | ------------------------------- | ------------------- | --------------------------------- | +| `searchTerm` | `string` | _required_ | Current search input value | +| `setSearchTerm` | `(term: string) => void` | _required_ | Callback for search input changes | +| `selectedCategory` | `string \| null` | _required_ | Currently selected category | +| `setSelectedCategory` | `(cat: string \| null) => void` | _required_ | Callback for category changes | +| `categories` | `string[]` | _required_ | List of available category labels | +| `postsCount` | `number` | — | Number of results to display | +| `placeholder` | `string` | `'Search blogs...'` | Search input placeholder text | +| `className` | `string` | — | Container CSS class | +| `classNames` | `object` | — | CSS overrides (see below) | + +**`classNames` keys:** `input`, `categoryContainer`, `pill`, `activePill`, `inactivePill`, +`postsCount` + +--- + +##### `BlogPlaceholder` + +Renders animated skeleton loading cards. + +| Prop | Type | Default | Description | +| ----------- | -------- | ------- | ------------------------------- | +| `count` | `number` | `3` | Number of placeholder skeletons | +| `className` | `string` | — | Additional CSS classes | + +--- #### Hooks -- `useBlogs(blogs: BlogMeta[])` - Provides search and filter functionality +##### `useBlogs(blogsMeta: BlogMeta[])` + +A client-side hook that provides search and category filter state management with debounced search +(500ms). + +**Parameters:** + +- `blogsMeta` (BlogMeta[]): Array of blog metadata to filter -See the [React Package](/docs/react-package) section for detailed examples of each component and -hook. +**Returns:** + +| Field | Type | Description | +| --------------------- | ------------------------------- | -------------------------------------- | +| `metadata` | `BlogMeta[]` | Filtered blog posts | +| `searchTerm` | `string` | Current search input value | +| `setSearchTerm` | `(term: string) => void` | Update the search term | +| `selectedCategory` | `string \| null` | Currently selected category | +| `setSelectedCategory` | `(cat: string \| null) => void` | Update selected category | +| `categories` | `string[]` | All unique categories from input blogs | + +--- + +#### Utility Functions + +##### `filterBlogs(blogs: BlogMeta[], searchTerm?: string, selectedCategory?: string | null): BlogMeta[]` + +Pure utility to filter blogs by search term (matches title and description) and/or category. Useful +for server-side filtering or custom implementations outside of `useBlogs`. + +##### `getAvailableCategories(blogs: BlogMeta[]): string[]` + +Returns a deduplicated array of all categories across the provided blog metadata. + +--- ### Types +All types are exported from both `@haroonwaves/blog-kit-core` and `@haroonwaves/blog-kit-react`. + ```typescript interface BlogMeta { title: string; @@ -96,11 +212,10 @@ interface BlogMeta { interface Blog { metadata: BlogMeta; content: string; - readingTime: string; } interface BlogConfig { contentDirectory: string; - blogSubdirectory?: string; + blogSubdirectory?: string; // defaults to 'blog' } ``` diff --git a/app/content/docs/core-package.md b/app/content/docs/core-package.md deleted file mode 100644 index e0feed0..0000000 --- a/app/content/docs/core-package.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: Blog Kit Core Package -description: Blog Kit's Core utilities for parsing markdown blog files and calculating reading time. -date: 2024-01-04 ---- - -## Core Package - -The `@haroonwaves/blog-kit-core` package provides essential utilities for parsing markdown blog -files with frontmatter metadata and automatically calculating reading time. It works in both -server-side (Node.js) and client-side (browser) environments, making it flexible for various use -cases. - -### Basic Usage - -#### Server-Side (SSR/SSG) - -For server-side rendering (Next.js, Node.js scripts, etc.): - -```typescript -import { getAllBlogsMeta, getBlog } from '@haroonwaves/blog-kit-core'; - -const config = { - contentDirectory: './content', - blogSubdirectory: 'blog', // optional, defaults to 'blog' -}; - -// Get all blog metadata -const blogsMeta = getAllBlogsMeta(config); - -// Get a specific blog post -const blog = getBlog('my-blog-post', config); -``` - -#### Client-Side - -For client-side usage (when you have markdown content as strings): - -```typescript -import { extractBlogMeta, extractBlog } from '@haroonwaves/blog-kit-core'; - -// Extract metadata from markdown content -const blogMeta = extractBlogMeta(markdownContent, 'my-blog-post'); - -// Extract full blog data from markdown content -const blog = extractBlog(markdownContent, 'my-blog-post'); -``` - -### Blog File Format - -Your markdown files should include frontmatter: - -```markdown ---- -title: My Blog Post -description: A brief description of the post -date: 2024-01-15 -categories: - - Technology - - Web Development ---- - -# My Blog Post - -Your markdown content here... -``` - -**Required frontmatter fields:** - -- `title` (string): The blog post title -- `description` (string): A brief description/summary -- `date` (string): Publication date (ISO format recommended: YYYY-MM-DD) - -**Optional frontmatter fields:** - -- `categories` (string[]): Array of categories/tags for the post - -The parser automatically extracts this frontmatter and calculates reading time based on the content -length. diff --git a/app/content/docs/dark-mode.md b/app/content/docs/dark-mode.md deleted file mode 100644 index 10b4fb4..0000000 --- a/app/content/docs/dark-mode.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Dark Mode Setup -description: - Learn how to configure dark mode support for Blog Kit components in your Next.js or React - application with step-by-step setup instructions. -date: 2024-01-04 ---- - -## Dark Mode Setup - -Learn how to configure dark mode support for Blog Kit components in your application. This guide -covers setup for both Next.js (using next-themes) and vanilla React applications with custom theme -providers. - -This package uses a custom Tailwind `dark` variant that activates only when the `` element has -the `dark` class. Make sure your theme provider toggles `document.documentElement.classList`. - -### Next.js with next-themes - -```tsx -// app/providers.tsx -'use client'; - -import { ThemeProvider } from 'next-themes'; - -export function Providers({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} - -// app/layout.tsx -import { Providers } from './providers'; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - {children} - - - ); -} -``` - -### Vite/CRA with custom theme toggle - -```tsx -// ThemeProvider.tsx -import { createContext, useContext, useEffect, useState } from 'react'; - -const ThemeContext = createContext<{ - theme: 'light' | 'dark'; - toggleTheme: () => void; -}>({ theme: 'light', toggleTheme: () => {} }); - -export function ThemeProvider({ children }: { children: React.ReactNode }) { - const [theme, setTheme] = useState<'light' | 'dark'>('light'); - - useEffect(() => { - const stored = localStorage.getItem('theme') as 'light' | 'dark' | null; - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - const initialTheme = stored || (prefersDark ? 'dark' : 'light'); - setTheme(initialTheme); - document.documentElement.classList.toggle('dark', initialTheme === 'dark'); - }, []); - - const toggleTheme = () => { - const newTheme = theme === 'light' ? 'dark' : 'light'; - setTheme(newTheme); - localStorage.setItem('theme', newTheme); - document.documentElement.classList.toggle('dark', newTheme === 'dark'); - }; - - return {children}; -} - -export const useTheme = () => useContext(ThemeContext); -``` diff --git a/app/content/docs/getting-started.md b/app/content/docs/getting-started.md new file mode 100644 index 0000000..09a3396 --- /dev/null +++ b/app/content/docs/getting-started.md @@ -0,0 +1,173 @@ +--- +title: Getting Started +description: + Install Blog Kit, set up your markdown files, and build a fully working blog with Next.js in + minutes. A step-by-step guide from zero to production. +date: 2024-01-02 +--- + +## Getting Started + +This guide walks you from zero to a fully working blog. By the end, you'll have a blog list page and +individual blog post pages — all statically generated for maximum performance. + +### 1. Install the Packages + +You need two packages: `core` for reading your markdown files, and `react` for rendering them. + +```bash +npm install @haroonwaves/blog-kit-core @haroonwaves/blog-kit-react +# or +pnpm add @haroonwaves/blog-kit-core @haroonwaves/blog-kit-react +# or +yarn add @haroonwaves/blog-kit-core @haroonwaves/blog-kit-react +``` + +> **Note:** The React package requires **React 19+** as a peer dependency. + +### 2. Import the Styles + +Blog Kit ships two CSS files. Import them **before** your app's global CSS so the component styles +load correctly: + +```tsx +// In your root layout or entry file (e.g., layout.tsx, App.tsx) +import '@haroonwaves/blog-kit-react/dist/index.css'; // Prism theme (code block styling) +import '@haroonwaves/blog-kit-react/dist/style.css'; // Component styles (Tailwind classes) +import './globals.css'; // Your app's CSS +``` + +> **Why this order matters:** Blog Kit uses a `bk:` namespace for all its utility classes (e.g., +> `bk:text-2xl`), so library styles never clash with your app's Tailwind classes. Importing Blog +> Kit's CSS first lets your app's base typography serve as the foundation. + +### 3. Write Your First Blog Post + +Create a markdown file with frontmatter metadata. Blog Kit uses +[gray-matter](https://github.com/jonschlinkert/gray-matter) to parse frontmatter and +[reading-time](https://github.com/ngryman/reading-time) to calculate reading time automatically. + +**Project structure:** + +``` +your-project/ +├── content/ +│ └── blog/ +│ ├── hello-world.md +│ └── my-second-post.md +└── src/ + └── app/ + ├── blog/ + │ ├── page.tsx ← Blog list page + │ └── [slug]/ + │ └── page.tsx ← Blog post page + └── layout.tsx +``` + +**Example markdown file** (`content/blog/hello-world.md`): + +```markdown +--- +title: Hello World +description: My first post built with Blog Kit. +date: 2024-01-15 +categories: + - Technology + - Web Development +--- + +# Hello World + +Welcome to my blog! This is my first post built with Blog Kit. +``` + +**Required frontmatter fields:** + +- `title` (string): The blog post title +- `description` (string): A brief description/summary +- `date` (string): Publication date (ISO format recommended: YYYY-MM-DD) + +**Optional frontmatter fields:** + +- `categories` (string[]): Array of categories/tags for the post + +### 4. Create the Blog Configuration + +Define your blog configuration once and reuse it across pages: + +```ts +// lib/blog.ts (or wherever you keep shared config) +import type { BlogConfig } from '@haroonwaves/blog-kit-core'; + +export const blogConfig: BlogConfig = { + contentDirectory: process.cwd(), // The project root + blogSubdirectory: 'content/blog', // Path to your markdown files +}; +``` + +### 5. Build the Blog List Page + +```tsx +// app/blog/page.tsx +import { getAllBlogsMeta } from '@haroonwaves/blog-kit-core'; +import { BlogList } from '@haroonwaves/blog-kit-react'; +import Link from 'next/link'; +import { blogConfig } from '@/lib/blog'; + +export default function BlogPage() { + const blogsMeta = getAllBlogsMeta(blogConfig); + + return ( + {children}} + /> + ); +} +``` + +That's it — `BlogList` renders a responsive list of cards with category badges, reading time, and +dates. No extra configuration needed. + +### 6. Build the Blog Post Page + +```tsx +// app/blog/[slug]/page.tsx +import { getAllBlogsMeta, getBlog } from '@haroonwaves/blog-kit-core'; +import { BlogRenderer } from '@haroonwaves/blog-kit-react'; +import { notFound } from 'next/navigation'; +import { blogConfig } from '@/lib/blog'; + +export function generateStaticParams() { + const blogsMeta = getAllBlogsMeta(blogConfig); + return blogsMeta.map((meta) => ({ slug: meta.slug })); +} + +export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + + const blog = getBlog(slug, blogConfig); + + if (!blog) notFound(); + + return ( +
+ +
+ ); +} +``` + +`BlogRenderer` automatically handles markdown rendering with syntax highlighting, GFM support, +heading anchor links, and beautiful typography — all out of the box. + +### Next Steps + +Your blog is now fully functional! Here's what to explore next: + +- **[Guides](/docs/guides)** — Add search/filtering, dark mode, and customize component styling +- **[API Reference](/docs/api-reference)** — Complete reference for all functions, components, + hooks, and types diff --git a/app/content/docs/guides.md b/app/content/docs/guides.md new file mode 100644 index 0000000..cfa72be --- /dev/null +++ b/app/content/docs/guides.md @@ -0,0 +1,239 @@ +--- +title: Guides +description: + Learn how to add search and filtering, customize component styling, and set up dark mode for your + Blog Kit blog. +date: 2024-01-04 +--- + +## Guides + +Practical guides for common tasks. If you haven't set up your blog yet, start with the +[Getting Started](/docs/getting-started) guide first. + +--- + +### Adding Search & Filtering + +The `useBlogs` hook provides instant client-side search and category filtering. Pair it with the +`Filter` and `BlogList` components for a complete interactive blog list. + +```tsx +'use client'; + +import { BlogList, Filter, useBlogs } from '@haroonwaves/blog-kit-react'; +import type { BlogMeta } from '@haroonwaves/blog-kit-react'; + +export default function BlogListPage({ blogsMeta }: { blogsMeta: BlogMeta[] }) { + const { metadata, searchTerm, setSearchTerm, selectedCategory, setSelectedCategory, categories } = + useBlogs(blogsMeta); + + return ( +
+ + + +
+ ); +} +``` + +Then pass metadata from a server component: + +```tsx +// app/blog/page.tsx (Server Component) +import { getAllBlogsMeta } from '@haroonwaves/blog-kit-core'; +import { blogConfig } from '@/lib/blog'; +import BlogListPage from './blog-list-page'; + +export default function BlogPage() { + const blogsMeta = getAllBlogsMeta(blogConfig); + return ; +} +``` + +> [!TIP] **Which rendering strategy should I use?** +> +> 1. **Client-Side Filtering (< 500 posts)**: Keep your blog list page static (SSG) but use the +> `useBlogs` hook to handle searching. This makes search instant and keeps your site 100% static. +> **This is the recommended approach for most blogs.** +> 2. **Dynamic Filtering (SSR)**: Only recommended if you have thousands of blog posts and want to +> avoid sending all metadata to the user's browser. +> 3. **Static Rendering (SSG)**: For individual blog post pages, always use SSG via +> `generateStaticParams()` for maximum speed. + +#### Server-Side Filtering + +If you prefer to filter on the server (e.g., for very large blogs), you can use the `filterBlogs` +and `getAvailableCategories` utility functions directly: + +```tsx +import { filterBlogs, getAvailableCategories } from '@haroonwaves/blog-kit-react'; +import { getAllBlogsMeta } from '@haroonwaves/blog-kit-core'; + +const allBlogs = getAllBlogsMeta(blogConfig); +const categories = getAvailableCategories(allBlogs); +const filtered = filterBlogs(allBlogs, searchTerm, selectedCategory); +``` + +--- + +### Customizing Components + +#### Overriding BlogRenderer Elements + +The `BlogRenderer` renders markdown using styled HTML elements. You can override any element by +passing custom components through the `components` prop: + +```tsx +import { BlogRenderer } from '@haroonwaves/blog-kit-react'; +import type { ComponentProps } from 'react'; + +function BlogPost({ content, metadata }) { + const customComponents = { + // Custom blockquote with a different style + blockquote: (props: ComponentProps<'blockquote'>) => ( +
+ ), + + // Custom link that opens in a new tab + a: (props: ComponentProps<'a'>) => ( + + ), + }; + + return ; +} +``` + +You can override any HTML element: `h1`–`h6`, `p`, `a`, `blockquote`, `code`, `pre`, `img`, `table`, +`ul`, `ol`, `li`, `strong`, `em`, `del`, `hr`, `br`, `input` (checkboxes), and all table elements +(`thead`, `tbody`, `tr`, `th`, `td`). + +#### Using classNames Props + +Several components accept a `classNames` prop for fine-grained style overrides without replacing the +entire component: + +**BlogList:** + +```tsx + +``` + +**Filter:** + +```tsx + +``` + +#### Loading Placeholders + +Use `BlogPlaceholder` to show skeleton cards while your data loads: + +```tsx +import { BlogPlaceholder } from '@haroonwaves/blog-kit-react'; + +function LoadingState() { + return ; +} +``` + +--- + +### Dark Mode Setup + +Blog Kit components use a custom Tailwind `dark` variant that activates when the `` element +has the `dark` class. You need to set up a theme provider that toggles +`document.documentElement.classList`. + +#### Next.js with next-themes + +```tsx +// app/providers.tsx +'use client'; + +import { ThemeProvider } from 'next-themes'; + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +// app/layout.tsx +import { Providers } from './providers'; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} +``` + +#### Vite/CRA with Custom Theme Toggle + +```tsx +// ThemeProvider.tsx +import { createContext, useContext, useEffect, useState } from 'react'; + +const ThemeContext = createContext<{ + theme: 'light' | 'dark'; + toggleTheme: () => void; +}>({ theme: 'light', toggleTheme: () => {} }); + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setTheme] = useState<'light' | 'dark'>('light'); + + useEffect(() => { + const stored = localStorage.getItem('theme') as 'light' | 'dark' | null; + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const initialTheme = stored || (prefersDark ? 'dark' : 'light'); + setTheme(initialTheme); + document.documentElement.classList.toggle('dark', initialTheme === 'dark'); + }, []); + + const toggleTheme = () => { + const newTheme = theme === 'light' ? 'dark' : 'light'; + setTheme(newTheme); + localStorage.setItem('theme', newTheme); + document.documentElement.classList.toggle('dark', newTheme === 'dark'); + }; + + return {children}; +} + +export const useTheme = () => useContext(ThemeContext); +``` diff --git a/app/content/docs/installation.md b/app/content/docs/installation.md deleted file mode 100644 index 7180fb0..0000000 --- a/app/content/docs/installation.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Blog Kit Installation -description: How to install Blog Kit packages using node package manager. -date: 2024-01-02 ---- - -## Installation - -Get started with Blog Kit by installing the packages you need. Designed primarily for Next.js -applications, you can choose between the core package for server-side markdown parsing or the React -package for pre-built UI components. - -### Core Package - -```bash -npm install @haroonwaves/blog-kit-core -# or -pnpm add @haroonwaves/blog-kit-core -# or -yarn add @haroonwaves/blog-kit-core -``` - -### React Package - -```bash -npm install @haroonwaves/blog-kit-react -# or -pnpm add @haroonwaves/blog-kit-react -# or -yarn add @haroonwaves/blog-kit-react -``` - -**Note:** The React package requires React 19+ as a peer dependency. - -### Styling - -The package includes Prism.js CSS for syntax highlighting and Tailwind CSS for component styling. -Import these styles **before** your app's global CSS: - -```tsx -// In your root layout or entry file (e.g., layout.tsx, App.tsx) -import '@haroonwaves/blog-kit-react/dist/index.css'; // Prism syntax highlighting -import '@haroonwaves/blog-kit-react/dist/style.css'; // Component styles -import './globals.css'; // Your app's CSS -``` - -> **Important:** Blog Kit uses a dedicated `bk:` namespace for all its utility classes (e.g., -> `bk:text-2xl`). This ensures that library styles never clash with your app's own Tailwind -> utilities. While the `bk:` namespace makes styling robust, importing Blog Kit's CSS **before** -> your app's global CSS remains standard practice so that your app's base typography can serve as -> the foundation. diff --git a/app/content/docs/overview.md b/app/content/docs/overview.md index bd8e2fe..60330e5 100644 --- a/app/content/docs/overview.md +++ b/app/content/docs/overview.md @@ -8,10 +8,10 @@ date: 2024-01-01 ## Build Your Professional Blog with Next.js & Blog Kit -Blog Kit is a modern, Next.js-first toolkit designed to help you **build your own professional blog** -with markdown in minutes. Whether you me creating a personal blog, documentation site, or content -platform, Blog Kit provides all the essential tools you need to build a high-performance blog with -minimal configuration. +Blog Kit is a modern, Next.js-first toolkit designed to help you **build your own professional +blog** with markdown in minutes. Whether you're creating a personal blog, documentation site, or +content platform, Blog Kit provides all the essential tools you need to build a high-performance +blog with minimal configuration. **Perfect for developers, writers, and content creators** who want full control over their blog using the power of Next.js and React server components. @@ -40,9 +40,10 @@ using the power of Next.js and React server components. - ✅ Raw HTML rendering support (using [rehype-raw](https://github.com/rehypejs/rehype-raw)) - ✅ Automatic heading IDs for anchor links (using [rehype-slug-custom-id](https://github.com/valeriangalliat/rehype-slug-custom-id)) -- ✅ Responsive blog card components -- ✅ Search and filter functionality via `useBlogs` hook -- ✅ Loading placeholders +- ✅ Beautiful, stateless `BlogList` for high-performance rendering +- ✅ Customizable `Filter` component with built-in search, categories, and results count +- ✅ `useBlogs` hook and shared utilities for easy search/filter integration +- ✅ Loading placeholders and responsive blog cards - ✅ Customizable styling with Tailwind CSS classes - ✅ TypeScript support with full type definitions - ✅ Work seamlessly with Next.js App Router and Server Components @@ -73,8 +74,8 @@ Blog Kit is perfect for: Ready to build your blog? Get started in minutes: -1. **[Install](/docs/installation)** - Add Blog Kit to your project -2. **[Quick Start](/docs/quick-start)** - Follow our step-by-step guide -3. **[API Reference](/docs/api-reference)** - Explore all available features +1. **[Getting Started](/docs/getting-started)** — Install, configure, and build your first blog +2. **[Guides](/docs/guides)** — Add search, dark mode, and customize components +3. **[API Reference](/docs/api-reference)** — Complete reference for all functions and components -Or jump straight to the [Quick Start Guide](/docs/quick-start) to see Blog Kit in action. +Or jump straight to the [Getting Started Guide](/docs/getting-started) to see Blog Kit in action. diff --git a/app/content/docs/quick-start.md b/app/content/docs/quick-start.md deleted file mode 100644 index cb70c55..0000000 --- a/app/content/docs/quick-start.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: Quick Start -description: - Build your own blog in minutes with this step-by-step guide. Learn how to set up Blog Kit with - Next.js and start creating your markdown blog today. -date: 2024-01-03 ---- - -## Quick Start with Next.js - -Get your blog up and running in minutes with Blog Kit. This guide shows you the fastest way to -integrate Blog Kit into your Next.js application using Server Components and Static Site Generation -(SSG) for optimal performance and SEO. - -### Project Structure Example - -``` -/app -/content - /blog - my-post.md -``` - -### Next.js SSG (Recommended) - -The easiest way to get started is with Next.js using Static Site Generation (SSG): - -```tsx -// app/blog/page.tsx -import { getAllBlogsMeta } from '@haroonwaves/blog-kit-core'; -import { BlogList } from '@haroonwaves/blog-kit-react'; -import Link from 'next/link'; - -export default function BlogPage() { - const blogsMeta = getAllBlogsMeta({ - contentDirectory: process.cwd(), - blogSubdirectory: 'content/blog', - }); - - return ( - {children}} - /> - ); -} -``` - -```tsx -// app/blog/[slug]/page.tsx -import { getAllBlogsMeta, getBlog } from '@haroonwaves/blog-kit-core'; -import { BlogRenderer } from '@haroonwaves/blog-kit-react'; -import { notFound } from 'next/navigation'; - -export function generateStaticParams() { - const blogsMeta = getAllBlogsMeta({ - contentDirectory: process.cwd(), - blogSubdirectory: 'content/blog', - }); - return blogsMeta.map((meta) => ({ slug: meta.slug })); -} - -export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) { - const { slug } = await params; - - const blog = getBlog(slug, { - contentDirectory: process.cwd(), - blogSubdirectory: 'content/blog', - }); - - if (!blog) notFound(); - - return ( -
-

{blog.metadata.title}

- -
- ); -} -``` - -For more examples, see [Next.js SSG Example](#nextjs-ssg-example-static-site-generation) or -[Next.js SSR Example](#nextjs-ssr-example-server-side-rendering). diff --git a/app/content/docs/react-package.md b/app/content/docs/react-package.md deleted file mode 100644 index bef2ef2..0000000 --- a/app/content/docs/react-package.md +++ /dev/null @@ -1,308 +0,0 @@ ---- -title: Blog Kit React Package -description: Blog Kit's React components and hooks for rendering blogs and documentation sites. -date: 2024-01-05 ---- - -## React Package - -The `@haroonwaves/blog-kit-react` package offers a collection of production-ready React components -and hooks for building beautiful blog interfaces. From markdown rendering with syntax highlighting -to search functionality, these components are designed to work seamlessly with any React framework. - -### BlogRenderer - -Render markdown content with syntax highlighting and beautiful styling: - -```tsx -import { BlogRenderer } from '@haroonwaves/blog-kit-react'; - -function BlogPost({ content }) { - return ; -} -``` - -#### Customizing Components - -You can override any default component by passing custom components through the `components` prop: - -```tsx -import { BlogRenderer } from '@haroonwaves/blog-kit-react'; -import type { ComponentProps } from 'react'; - -function BlogPost({ content, metadata }) { - // Custom component overrides - const customComponents = { - // Custom blockquote with a different style - blockquote: (props: ComponentProps<'blockquote'>) => ( -
- ), - }; - - return ; -} -``` - -**Props:** - -- `content` (string, required): Blog content to render -- `metadata` (BlogMeta, required): Blog meta info to render -- `className` (string, optional): Additional CSS classes -- `components` (object, optional): Custom component overrides -- `showCategory` (boolean, optional): Show category badge (default: true) -- `showReadingTime` (boolean, optional): Show reading time (default: true) -- `showDate` (boolean, optional): Show publication date (default: true) - -### BlogCard - -Display a single blog post card: - -```tsx -import { BlogCard } from '@haroonwaves/blog-kit-react'; - -function BlogCardExample({ blogMeta }) { - return ; -} -``` - -**Props:** - -- `metadata` (BlogMeta, required): Blog metadata object -- `basePath` (string, optional): Base path for blog links (default: '/blog') -- `renderLink` (function, optional): Custom link renderer (useful for Next.js Link) -- `className` (string, optional): Additional CSS classes -- `showCategory` (boolean, optional): Show category badge (default: true) -- `showReadingTime` (boolean, optional): Show reading time (default: true) -- `showDate` (boolean, optional): Show publication date (default: true) - -### BlogList - -Display a list of blog posts: - -```tsx -import { BlogList } from '@haroonwaves/blog-kit-react'; - -function BlogListExample({ blogsMeta }) { - return ; -} -``` - -**Props:** - -- `metadata` (BlogMeta[], required): Array of blog metadata -- `basePath` (string, optional): Base path for blog links (default: '/blog') -- `renderLink` (function, optional): Custom link renderer -- `className` (string, optional): Additional CSS classes -- `emptyMessage` (string, optional): Message when no blogs (default: 'No blog posts found.') -- `cardProps` (object, optional): Props to pass to each BlogCard - -### BlogPlaceholder - -Show loading placeholders while blogs are loading: - -```tsx -import { BlogPlaceholder } from '@haroonwaves/blog-kit-react'; - -function LoadingBlogs() { - return ; -} -``` - -**Props:** - -- `count` (number, optional): Number of placeholder cards (default: 3) -- `className` (string, optional): Additional CSS classes - -### useBlogs Hook - -Filter and search through blog posts: - -```tsx -import { useBlogs } from '@haroonwaves/blog-kit-react'; - -function BlogSearch({ blogsMeta }) { - const { metadata, searchTerm, setSearchTerm, selectedCategory, setSelectedCategory, categories } = - useBlogs(blogsMeta); - - return ( -
- setSearchTerm(e.target.value)} - placeholder="Search blogs..." - /> - - -
- ); -} -``` - -**Returns:** - -- `metadata` (BlogMeta[]): Filtered blog posts metadata -- `searchTerm` (string): Current search term -- `setSearchTerm` (function): Update search term -- `selectedCategory` (string | null): Selected category filter -- `setSelectedCategory` (function): Update category filter -- `categories` (string[]): Available categories from blogs - -### Next.js Integration - -For Next.js projects, use a custom link renderer: - -```tsx -import Link from 'next/link'; -import { BlogCard } from '@haroonwaves/blog-kit-react'; - -function NextBlogCard({ blog }) { - return ( - {children}} - /> - ); -} -``` - -### Next.js SSG Example (Static Site Generation) - -For Next.js with static site generation, use server components and `generateStaticParams`: - -**Blog List Page** (`app/blog/page.tsx`): - -```tsx -import { getAllBlogsMeta } from '@haroonwaves/blog-kit-core'; -import { BlogList } from '@haroonwaves/blog-kit-react'; -import Link from 'next/link'; - -export default function BlogListPage() { - const blogsMeta = getAllBlogsMeta({ - contentDirectory: process.cwd(), - blogSubdirectory: 'content/blog', - }); - - return ( -
-
-

Blogs

- {children}} - /> -
-
- ); -} -``` - -**Blog Post Page** (`app/blog/[slug]/page.tsx`): - -```tsx -import { getAllBlogsMeta, getBlog } from '@haroonwaves/blog-kit-core'; -import { BlogRenderer } from '@haroonwaves/blog-kit-react'; -import { notFound } from 'next/navigation'; -import Link from 'next/link'; -import type { Metadata } from 'next'; - -const blogConfig = { - contentDirectory: process.cwd(), - blogSubdirectory: 'content/blog', -}; - -export function generateStaticParams() { - const blogsMeta = getAllBlogsMeta(blogConfig); - return blogsMeta.map((meta) => ({ - slug: meta.slug, - })); -} - -export async function generateMetadata({ - params, -}: { - params: Promise<{ slug: string }>; -}): Promise { - const { slug } = await params; - - const blog = getBlog(slug, blogConfig); - - if (!blog) { - return { - title: 'Blog Post Not Found', - }; - } - - return { - title: `${blog.metadata.title} | Blog Kit`, - description: blog.metadata.description, - openGraph: { - title: blog.metadata.title, - description: blog.metadata.description, - type: 'article', - publishedTime: blog.metadata.date, - }, - }; -} - -export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) { - const { slug } = await params; - - const blog = getBlog(slug, blogConfig); - - if (!blog) notFound(); - - const { metadata, content } = blog; - - return ( -
- -
- ); -} -``` - -### Next.js SSR Example (Server-Side Rendering) - -For server-side rendering, use the same functions but without `generateStaticParams`: - -```tsx -// app/blog/[slug]/page.tsx -import { getBlog } from '@haroonwaves/blog-kit-core'; -import { BlogRenderer } from '@haroonwaves/blog-kit-react'; -import { notFound } from 'next/navigation'; - -export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) { - const { slug } = await params; - - const blog = getBlog(slug, { - contentDirectory: process.cwd(), - blogSubdirectory: 'content/blog', - }); - - if (!blog) notFound(); - - return ( -
- -
- ); -} -``` - -**Note:** SSG is recommended for blogs as it pre-renders pages at build time for better performance. - diff --git a/app/package.json b/app/package.json index 967d211..547b26d 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "@haroonwaves/blog-kit-app", - "version": "1.3.1", + "version": "1.4.0", "private": true, "scripts": { "dev": "next dev", diff --git a/app/src/app/docs/page.tsx b/app/src/app/docs/page.tsx index f3c7b25..7f78200 100644 --- a/app/src/app/docs/page.tsx +++ b/app/src/app/docs/page.tsx @@ -1,15 +1,5 @@ import Link from 'next/link'; -import { - BookOpen, - Download, - Zap, - Package, - Layers, - FileCode, - Code, - ArrowRight, - Sparkles, -} from 'lucide-react'; +import { BookOpen, Zap, Compass, FileCode, ArrowRight, Sparkles } from 'lucide-react'; const quickLinks = [ { @@ -21,53 +11,29 @@ const quickLinks = [ iconColor: 'text-blue-600 dark:text-blue-400', }, { - title: 'Installation', - description: 'Add Blog Kit to your project in minutes', - href: '/docs/installation', - icon: Download, + title: 'Getting Started', + description: 'Install, configure, and build your blog in minutes', + href: '/docs/getting-started', + icon: Zap, color: 'from-green-500/10 to-emerald-500/10 border-green-500/20', iconColor: 'text-green-600 dark:text-green-400', }, { - title: 'Quick Start', - description: 'Get up and running with a step-by-step guide', - href: '/docs/quick-start', - icon: Zap, - color: 'from-yellow-500/10 to-orange-500/10 border-yellow-500/20', - iconColor: 'text-yellow-600 dark:text-yellow-400', - }, - { - title: 'Core Package', - description: 'Parse markdown files and extract metadata', - href: '/docs/core-package', - icon: Package, + title: 'Guides', + description: 'Search, filtering, dark mode, and customization', + href: '/docs/guides', + icon: Compass, color: 'from-purple-500/10 to-pink-500/10 border-purple-500/20', iconColor: 'text-purple-600 dark:text-purple-400', }, - { - title: 'React Package', - description: 'Beautiful components for rendering blogs', - href: '/docs/react-package', - icon: Layers, - color: 'from-indigo-500/10 to-blue-500/10 border-indigo-500/20', - iconColor: 'text-indigo-600 dark:text-indigo-400', - }, { title: 'API Reference', - description: 'Explore all available features and APIs', + description: 'Complete reference for all functions, components, and types', href: '/docs/api-reference', icon: FileCode, color: 'from-red-500/10 to-rose-500/10 border-red-500/20', iconColor: 'text-red-600 dark:text-red-400', }, - { - title: 'Dark Mode', - description: 'Implement beautiful dark mode support', - href: '/docs/dark-mode', - icon: Code, - color: 'from-slate-500/10 to-gray-500/10 border-slate-500/20', - iconColor: 'text-slate-600 dark:text-slate-400', - }, ]; export default function DocsPage() { @@ -94,11 +60,11 @@ export default function DocsPage() { {/* Quick Start CTA */}
- Quick Start Guide + Getting Started

Explore the Documentation

-
+
{quickLinks.map((link) => { const Icon = link.icon; return ( diff --git a/app/src/components/docs-sidebar.tsx b/app/src/components/docs-sidebar.tsx index bd994f2..7240e0c 100644 --- a/app/src/components/docs-sidebar.tsx +++ b/app/src/components/docs-sidebar.tsx @@ -3,28 +3,14 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { cn } from '@/lib/utils'; -import { - BookOpen, - Download, - Zap, - Package, - Layers, - FileCode, - Sparkles, - Code, - Menu, - X, -} from 'lucide-react'; +import { BookOpen, Zap, Compass, FileCode, Sparkles, Menu, X } from 'lucide-react'; import { useState } from 'react'; const sections = [ { title: 'Overview', href: '/docs/overview', icon: BookOpen }, - { title: 'Installation', href: '/docs/installation', icon: Download }, - { title: 'Quick Start', href: '/docs/quick-start', icon: Zap }, - { title: 'Core Package', href: '/docs/core-package', icon: Package }, - { title: 'React Package', href: '/docs/react-package', icon: Layers }, + { title: 'Getting Started', href: '/docs/getting-started', icon: Zap }, + { title: 'Guides', href: '/docs/guides', icon: Compass }, { title: 'API Reference', href: '/docs/api-reference', icon: FileCode }, - { title: 'Dark Mode', href: '/docs/dark-mode', icon: Code }, ]; export function DocsSidebar() { diff --git a/package.json b/package.json index 72028fe..c3c1b65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blog-kit", - "version": "1.3.1", + "version": "1.4.0", "private": true, "description": "The ultimate Next.js toolkit for building professional blog systems with markdown.", "main": "index.js", diff --git a/packages/blog-kit-core/package.json b/packages/blog-kit-core/package.json index 91665e5..3e4b137 100644 --- a/packages/blog-kit-core/package.json +++ b/packages/blog-kit-core/package.json @@ -1,6 +1,6 @@ { "name": "@haroonwaves/blog-kit-core", - "version": "1.3.1", + "version": "1.4.0", "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", diff --git a/packages/blog-kit-react/README.md b/packages/blog-kit-react/README.md index 05dd1c1..e5a9120 100644 --- a/packages/blog-kit-react/README.md +++ b/packages/blog-kit-react/README.md @@ -1,7 +1,7 @@ # @haroonwaves/blog-kit-react React components and hooks for rendering markdown blogs. Includes blog renderer with syntax -highlighting, blog cards, lists, and search functionality. +highlighting, blog cards, lists with built-in search, filtering, and customization. ## Installation diff --git a/packages/blog-kit-react/package.json b/packages/blog-kit-react/package.json index f146e88..90a8bea 100644 --- a/packages/blog-kit-react/package.json +++ b/packages/blog-kit-react/package.json @@ -1,6 +1,6 @@ { "name": "@haroonwaves/blog-kit-react", - "version": "1.3.1", + "version": "1.4.0", "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", diff --git a/packages/blog-kit-react/src/components/BlogList.tsx b/packages/blog-kit-react/src/components/BlogList.tsx index 2c674f6..de8e019 100644 --- a/packages/blog-kit-react/src/components/BlogList.tsx +++ b/packages/blog-kit-react/src/components/BlogList.tsx @@ -1,42 +1,75 @@ +import React from 'react'; import { BlogCard, type BlogCardProps } from './BlogCard'; import type { BlogMeta } from '../types'; export interface BlogListProps { metadata: BlogMeta[]; + title?: string; + description?: string; basePath?: string; renderLink?: BlogCardProps['renderLink']; className?: string; emptyMessage?: string; cardProps?: Omit; + classNames?: { + // cardContainer: string; + title?: string; + description?: string; + }; } +/** + * A pure, stateless BlogList component that renders a list of blog cards. + * Accepts metadata and renders it exactly as provided. + */ export function BlogList({ metadata, + title, + description, basePath = '/blog', renderLink, className = '', emptyMessage = 'No blog posts found.', cardProps, + classNames = {}, }: BlogListProps) { - if (metadata.length === 0) { - return ( -
- {emptyMessage} -
- ); - } - return ( -
- {metadata.map((meta) => ( - - ))} +
+ {title && ( +

+ {title} +

+ )} + + {description && ( +

+ {description} +

+ )} + + {metadata.length === 0 ? ( +
+ {emptyMessage} +
+ ) : ( +
+ {metadata.map((meta) => ( + + ))} +
+ )}
); } diff --git a/packages/blog-kit-react/src/components/BlogRenderer.tsx b/packages/blog-kit-react/src/components/BlogRenderer.tsx index 3d3dc48..252cfb7 100644 --- a/packages/blog-kit-react/src/components/BlogRenderer.tsx +++ b/packages/blog-kit-react/src/components/BlogRenderer.tsx @@ -67,11 +67,17 @@ const defaultComponents = { ), ul: (props: ComponentProps<'ul'>) => ( -
    +
      ), ol: (props: ComponentProps<'ol'>) => ( -
        +
          ), li: (props: ComponentProps<'li'>) =>
        1. , @@ -151,13 +157,19 @@ const defaultComponents = { ), thead: (props: ComponentProps<'thead'>) => ( - + ), tbody: (props: ComponentProps<'tbody'>) => , tr: (props: ComponentProps<'tr'>) => ( - + ), th: (props: ComponentProps<'th'>) => ( diff --git a/packages/blog-kit-react/src/components/Filter.tsx b/packages/blog-kit-react/src/components/Filter.tsx new file mode 100644 index 0000000..36fe688 --- /dev/null +++ b/packages/blog-kit-react/src/components/Filter.tsx @@ -0,0 +1,102 @@ +'use client'; + +import React from 'react'; + +export interface FilterProps { + searchTerm: string; + setSearchTerm: (searchTerm: string) => void; + selectedCategory: string | null; + setSelectedCategory: (selectedCategory: string | null) => void; + categories: string[]; + postsCount?: number; + className?: string; + placeholder?: string; + classNames?: { + input?: string; + categoryContainer?: string; + pill?: string; + activePill?: string; + inactivePill?: string; + postsCount?: string; + }; +} + +export function Filter({ + searchTerm, + setSearchTerm, + selectedCategory, + setSelectedCategory, + categories, + postsCount, + className = '', + placeholder = 'Search blogs...', + classNames = {}, +}: FilterProps) { + // Consolidate styles to avoid JSX bloat and repetition + const pillBase = `bk:px-2.5 bk:py-1 bk:cursor-pointer bk:rounded-md bk:text-xs bk:font-medium bk:transition-colors ${classNames.pill ?? ''}`; + const pillActive = `bk:bg-gray-700 bk:dark:bg-gray-200 bk:text-white bk:dark:text-gray-800 ${classNames.activePill ?? ''}`; + const pillInactive = `bk:bg-gray-100 bk:dark:bg-gray-700 bk:text-gray-500 bk:dark:text-gray-300 bk:hover:bg-gray-200 bk:dark:hover:bg-gray-600 ${classNames.inactivePill ?? ''}`; + + return ( +
          +
          + {/* Search input */} +
          + + + + setSearchTerm(e.target.value)} + placeholder={placeholder} + className={`bk:w-full bk:pl-8 bk:pr-4 bk:py-2 bk:rounded-md bk:border bk:border-gray-200 bk:dark:border-gray-700 bk:bg-white bk:dark:bg-gray-800 bk:text-sm bk:text-gray-700 bk:dark:text-gray-200 bk:placeholder-gray-400 bk:focus:outline-none bk:focus:border-gray-400 bk:dark:focus:border-gray-500 bk:transition-colors ${classNames.input ?? ''}`} + /> +
          + + {/* Category pills */} + {categories.length > 0 && ( +
          + + {categories.slice(0, 3).map((cat) => ( + + ))} +
          + )} +
          + + {/* Posts Count */} + {postsCount !== undefined && ( +

          + {postsCount} {postsCount === 1 ? 'post' : 'posts'} found +

          + )} +
          + ); +} diff --git a/packages/blog-kit-react/src/hooks/useBlogs.ts b/packages/blog-kit-react/src/hooks/useBlogs.ts index 1780b2a..63220dd 100644 --- a/packages/blog-kit-react/src/hooks/useBlogs.ts +++ b/packages/blog-kit-react/src/hooks/useBlogs.ts @@ -1,43 +1,26 @@ -import { useState, useEffect } from 'react'; +import { useState, useMemo } from 'react'; import type { BlogMeta } from '../types'; import { useDebounce } from './useDebounce'; +import { filterBlogs, getAvailableCategories } from '../utils/filter-utils'; export function useBlogs(blogsMeta: BlogMeta[]) { - const [filteredBlogs, setFilteredBlogs] = useState(blogsMeta); const [searchTerm, setSearchTerm] = useState(''); const [selectedCategory, setSelectedCategory] = useState(null); const debouncedSearchTerm = useDebounce(searchTerm, 500); - const getBlogCategories = (blog: BlogMeta): string[] => { - return blog.categories ?? []; - }; - - useEffect(() => { - let filtered = blogsMeta; - - if (debouncedSearchTerm) { - filtered = filtered.filter( - (blog) => - blog.title.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) || - blog.description.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) - ); - } - - if (selectedCategory) { - filtered = filtered.filter((blog) => { - const blogCategories = getBlogCategories(blog); - return blogCategories.includes(selectedCategory); - }); - } - - setFilteredBlogs(filtered); + // Compute filtered results using the pure utility + const metadata = useMemo(() => { + return filterBlogs(blogsMeta, debouncedSearchTerm, selectedCategory); }, [blogsMeta, debouncedSearchTerm, selectedCategory]); - const categories = Array.from(new Set(blogsMeta.flatMap((blog) => getBlogCategories(blog)))); + // Compute available categories + const categories = useMemo(() => { + return getAvailableCategories(blogsMeta); + }, [blogsMeta]); return { - metadata: filteredBlogs, + metadata, searchTerm, setSearchTerm, selectedCategory, diff --git a/packages/blog-kit-react/src/index.ts b/packages/blog-kit-react/src/index.ts index 770c95f..83e0249 100644 --- a/packages/blog-kit-react/src/index.ts +++ b/packages/blog-kit-react/src/index.ts @@ -11,8 +11,14 @@ export type { BlogCardProps } from './components/BlogCard'; export { BlogList } from './components/BlogList'; export type { BlogListProps } from './components/BlogList'; +export { Filter } from './components/Filter'; +export type { FilterProps } from './components/Filter'; + export { BlogPlaceholder } from './components/BlogPlaceholder'; export type { BlogPlaceholderProps } from './components/BlogPlaceholder'; // Hooks export { useBlogs } from './hooks/useBlogs'; + +// Utils +export * from './utils/filter-utils'; diff --git a/packages/blog-kit-react/src/utils/filter-utils.ts b/packages/blog-kit-react/src/utils/filter-utils.ts new file mode 100644 index 0000000..14ed103 --- /dev/null +++ b/packages/blog-kit-react/src/utils/filter-utils.ts @@ -0,0 +1,31 @@ +import type { BlogMeta } from '../types'; + +export function filterBlogs( + blogs: BlogMeta[], + searchTerm: string = '', + selectedCategory: string | null = null +): BlogMeta[] { + let filtered = blogs; + + if (searchTerm) { + const lowerSearch = searchTerm.toLowerCase(); + filtered = filtered.filter( + (blog) => + (blog.title?.toLowerCase() || '').includes(lowerSearch) || + (blog.description?.toLowerCase() || '').includes(lowerSearch) + ); + } + + if (selectedCategory) { + filtered = filtered.filter((blog) => { + const categories = blog.categories ?? []; + return categories.includes(selectedCategory); + }); + } + + return filtered; +} + +export function getAvailableCategories(blogs: BlogMeta[]): string[] { + return Array.from(new Set(blogs.flatMap((blog) => blog.categories ?? []))); +}