feat: RichText component for safe PocketBase HTML rendering
Add html-react-parser-backed RichText component that converts HTML strings from PocketBase rich-text fields into React elements without dangerouslySetInnerHTML. Replace raw <p> render in IntroSection and BioSection, and drop the invalid slug filters those collections lacked.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { RichText } from './ui/RichText';
|
||||
@@ -0,0 +1,45 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { RichText } from './RichText';
|
||||
|
||||
describe('RichText', () => {
|
||||
describe('rendering', () => {
|
||||
it('renders a paragraph from <p> tag', () => {
|
||||
render(<RichText html="<p>Hello world</p>" />);
|
||||
expect(screen.getByText('Hello world').tagName).toBe('P');
|
||||
});
|
||||
|
||||
it('renders bold text from <strong> tag', () => {
|
||||
render(<RichText html="<strong>Bold</strong>" />);
|
||||
expect(screen.getByText('Bold').tagName).toBe('STRONG');
|
||||
});
|
||||
|
||||
it('renders a link from <a> tag', () => {
|
||||
render(<RichText html='<a href="https://example.com">Link</a>' />);
|
||||
const link = screen.getByRole('link', { name: 'Link' });
|
||||
expect(link).toHaveAttribute('href', 'https://example.com');
|
||||
});
|
||||
|
||||
it('renders nested tags', () => {
|
||||
render(<RichText html="<p>Text with <em>emphasis</em></p>" />);
|
||||
expect(screen.getByText('emphasis').tagName).toBe('EM');
|
||||
});
|
||||
|
||||
it('renders nothing for empty string', () => {
|
||||
const { container } = render(<RichText html="" />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders multiple sibling elements', () => {
|
||||
render(<RichText html="<p>First</p><p>Second</p>" />);
|
||||
expect(screen.getByText('First')).toBeInTheDocument();
|
||||
expect(screen.getByText('Second')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('className passthrough', () => {
|
||||
it('applies className to the wrapper', () => {
|
||||
const { container } = render(<RichText html="<p>text</p>" className="prose" />);
|
||||
expect(container.firstChild).toHaveClass('prose');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import parse from 'html-react-parser';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* HTML string from PocketBase rich-text editor
|
||||
*/
|
||||
html: string;
|
||||
/**
|
||||
* CSS classes applied to the wrapper div
|
||||
*/
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a PocketBase rich-text HTML string as React elements.
|
||||
*/
|
||||
export function RichText({ html, className }: Props) {
|
||||
if (!html) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = parse(html);
|
||||
|
||||
if (className) {
|
||||
return <div className={className}>{parsed}</div>;
|
||||
}
|
||||
|
||||
return <>{parsed}</>;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export type { CardBackground } from './Card';
|
||||
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './Card';
|
||||
|
||||
export { Input, Textarea } from './Input';
|
||||
export { RichText } from './RichText';
|
||||
export type { ContainerSize, SectionBackground } from './Section';
|
||||
export { Container, Section } from './Section';
|
||||
|
||||
export { TechStackBrick, TechStackGrid } from './TechStack';
|
||||
|
||||
Reference in New Issue
Block a user