fix: storybook font rendering and shared fonts module #1
@@ -20,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"html-react-parser": "^6.1.0",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
|
|||||||
@@ -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 { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './Card';
|
||||||
|
|
||||||
export { Input, Textarea } from './Input';
|
export { Input, Textarea } from './Input';
|
||||||
|
export { RichText } from './RichText';
|
||||||
export type { ContainerSize, SectionBackground } from './Section';
|
export type { ContainerSize, SectionBackground } from './Section';
|
||||||
export { Container, Section } from './Section';
|
export { Container, Section } from './Section';
|
||||||
|
|
||||||
export { TechStackBrick, TechStackGrid } from './TechStack';
|
export { TechStackBrick, TechStackGrid } from './TechStack';
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import type { PageContentRecord } from '$shared/api';
|
import type { PageContentRecord } from '$shared/api';
|
||||||
import { getFirstRecord } from '$shared/api';
|
import { getFirstRecord } from '$shared/api';
|
||||||
|
import { RichText } from '$shared/ui';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bio section component.
|
* Bio section component.
|
||||||
* Displays personal biography content from PocketBase.
|
* Displays personal biography content from PocketBase.
|
||||||
*/
|
*/
|
||||||
export default async function BioSection() {
|
export default async function BioSection() {
|
||||||
const data = await getFirstRecord<PageContentRecord>('bio', {
|
const data = await getFirstRecord<PageContentRecord>('bio');
|
||||||
filter: 'slug = "bio"',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <RichText html={data.content} className="prose prose-lg max-w-none" />;
|
||||||
<div className="prose prose-lg dark:prose-invert max-w-none">
|
|
||||||
<p>{data.content}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import type { PageContentRecord } from '$shared/api';
|
import type { PageContentRecord } from '$shared/api';
|
||||||
import { getFirstRecord } from '$shared/api';
|
import { getFirstRecord } from '$shared/api';
|
||||||
|
import { RichText } from '$shared/ui';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intro section component.
|
* Intro section component.
|
||||||
* Displays primary introduction content from PocketBase.
|
* Displays primary introduction content from PocketBase.
|
||||||
*/
|
*/
|
||||||
export default async function IntroSection() {
|
export default async function IntroSection() {
|
||||||
const data = await getFirstRecord<PageContentRecord>('intro', {
|
const data = await getFirstRecord<PageContentRecord>('intro');
|
||||||
filter: 'slug = "intro"',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <RichText html={data.content} className="prose prose-lg max-w-none" />;
|
||||||
<div className="prose prose-lg dark:prose-invert max-w-none">
|
|
||||||
<p>{data.content}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3239,6 +3239,44 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"dom-serializer@npm:^3.0.0":
|
||||||
|
version: 3.1.1
|
||||||
|
resolution: "dom-serializer@npm:3.1.1"
|
||||||
|
dependencies:
|
||||||
|
domelementtype: "npm:^3.0.0"
|
||||||
|
domhandler: "npm:^6.0.0"
|
||||||
|
entities: "npm:^8.0.0"
|
||||||
|
checksum: 10c0/dc700204f0ef4a4c5a344bd8773703d5476dcca1a4af8b2d3fd9bcbbace833439b6ea3d3c48c4b387fa0b2456dd839caca354eed7f7c7f17bc47da8e217742ca
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"domelementtype@npm:^3.0.0":
|
||||||
|
version: 3.0.0
|
||||||
|
resolution: "domelementtype@npm:3.0.0"
|
||||||
|
checksum: 10c0/26e8ef992769c4f9bce941eb0cff7ce2ba3f1b3bf77710bb4b029055030625892e83da326cc36b1e444cf3bfdea7d1954791ee2227746387465da9929d16d954
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"domhandler@npm:6.0.1, domhandler@npm:^6.0.0":
|
||||||
|
version: 6.0.1
|
||||||
|
resolution: "domhandler@npm:6.0.1"
|
||||||
|
dependencies:
|
||||||
|
domelementtype: "npm:^3.0.0"
|
||||||
|
checksum: 10c0/8655204dd9612b55813d5880e3e87e134d6dfb2de4bd80f342b3c97f41b167576a8c66c0449c2423999953aedfcda290f7be253a6f9bf71e815afa85f939d44e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"domutils@npm:^4.0.2":
|
||||||
|
version: 4.0.2
|
||||||
|
resolution: "domutils@npm:4.0.2"
|
||||||
|
dependencies:
|
||||||
|
dom-serializer: "npm:^3.0.0"
|
||||||
|
domelementtype: "npm:^3.0.0"
|
||||||
|
domhandler: "npm:^6.0.0"
|
||||||
|
checksum: 10c0/59827827ecf15ed1f43f4cb8db374484b6089bf40e32cb41c8e381525aeb5ef5d029e4f9d5f74a418bf3217b87a6cbabdf5b4ebed0a018bc533bd6349c46a739
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1":
|
"dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1":
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
resolution: "dunder-proto@npm:1.0.1"
|
resolution: "dunder-proto@npm:1.0.1"
|
||||||
@@ -3288,6 +3326,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"entities@npm:^8.0.0":
|
||||||
|
version: 8.0.0
|
||||||
|
resolution: "entities@npm:8.0.0"
|
||||||
|
checksum: 10c0/938e631664c19451823344a351aeeafd74fae2d5fa51e4d5b6ff635afaefd4bacf0f609989888c04c42733f46ffdac15211608267ebb02488005891a4793e94d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"env-paths@npm:^2.2.0":
|
"env-paths@npm:^2.2.0":
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
resolution: "env-paths@npm:2.2.1"
|
resolution: "env-paths@npm:2.2.1"
|
||||||
@@ -4291,6 +4336,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"html-dom-parser@npm:7.1.0":
|
||||||
|
version: 7.1.0
|
||||||
|
resolution: "html-dom-parser@npm:7.1.0"
|
||||||
|
dependencies:
|
||||||
|
domhandler: "npm:6.0.1"
|
||||||
|
htmlparser2: "npm:12.0.0"
|
||||||
|
checksum: 10c0/e73b0c2e8bbe809ff877bf2483f6547f4797ee55c1c6d0f486d54ce7310e799c36986328f11dde1ce99608939a06efdf1d02c45a0abd0ec40b405b230c3dffdf
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"html-encoding-sniffer@npm:^6.0.0":
|
"html-encoding-sniffer@npm:^6.0.0":
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
resolution: "html-encoding-sniffer@npm:6.0.0"
|
resolution: "html-encoding-sniffer@npm:6.0.0"
|
||||||
@@ -4307,6 +4362,36 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"html-react-parser@npm:^6.1.0":
|
||||||
|
version: 6.1.0
|
||||||
|
resolution: "html-react-parser@npm:6.1.0"
|
||||||
|
dependencies:
|
||||||
|
domhandler: "npm:6.0.1"
|
||||||
|
html-dom-parser: "npm:7.1.0"
|
||||||
|
react-property: "npm:2.0.2"
|
||||||
|
style-to-js: "npm:1.1.21"
|
||||||
|
peerDependencies:
|
||||||
|
"@types/react": 0.14 || 15 || 16 || 17 || 18 || 19
|
||||||
|
react: 0.14 || 15 || 16 || 17 || 18 || 19
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@types/react":
|
||||||
|
optional: true
|
||||||
|
checksum: 10c0/1626454d3e3edf01b8e626b6f4a150b9ab013b4d379e5038506c93c3ce7cfb09a78abff079512ecbd4dd6d840c0bbcd55f722ee3302a70400c760e9109891b49
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"htmlparser2@npm:12.0.0":
|
||||||
|
version: 12.0.0
|
||||||
|
resolution: "htmlparser2@npm:12.0.0"
|
||||||
|
dependencies:
|
||||||
|
domelementtype: "npm:^3.0.0"
|
||||||
|
domhandler: "npm:^6.0.0"
|
||||||
|
domutils: "npm:^4.0.2"
|
||||||
|
entities: "npm:^8.0.0"
|
||||||
|
checksum: 10c0/3fcdce24c06fc4c9c42c8142d6c139104a2c30f901ce046cb0bdeaa8678445294aaf4506569464a5c853c8b1d89609f7306ea133efd966bf703f574a394dcff9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"http-cache-semantics@npm:^4.1.1":
|
"http-cache-semantics@npm:^4.1.1":
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
resolution: "http-cache-semantics@npm:4.2.0"
|
resolution: "http-cache-semantics@npm:4.2.0"
|
||||||
@@ -4390,6 +4475,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"inline-style-parser@npm:0.2.7":
|
||||||
|
version: 0.2.7
|
||||||
|
resolution: "inline-style-parser@npm:0.2.7"
|
||||||
|
checksum: 10c0/d884d76f84959517430ae6c22f0bda59bb3f58f539f99aac75a8d786199ec594ed648c6ab4640531f9fc244b0ed5cd8c458078e592d016ef06de793beb1debff
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"internal-slot@npm:^1.1.0":
|
"internal-slot@npm:^1.1.0":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "internal-slot@npm:1.1.0"
|
resolution: "internal-slot@npm:1.1.0"
|
||||||
@@ -5859,6 +5951,7 @@ __metadata:
|
|||||||
eslint: "npm:^9"
|
eslint: "npm:^9"
|
||||||
eslint-config-next: "npm:16.2.4"
|
eslint-config-next: "npm:16.2.4"
|
||||||
eslint-plugin-storybook: "npm:^10.3.5"
|
eslint-plugin-storybook: "npm:^10.3.5"
|
||||||
|
html-react-parser: "npm:^6.1.0"
|
||||||
jsdom: "npm:^29.0.2"
|
jsdom: "npm:^29.0.2"
|
||||||
lefthook: "npm:^2.1.6"
|
lefthook: "npm:^2.1.6"
|
||||||
next: "npm:16.2.4"
|
next: "npm:16.2.4"
|
||||||
@@ -6016,6 +6109,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"react-property@npm:2.0.2":
|
||||||
|
version: 2.0.2
|
||||||
|
resolution: "react-property@npm:2.0.2"
|
||||||
|
checksum: 10c0/27a3dfa68d29d45fc3582552715203291d26c6f1b228fdb6775e7ca19b10753141dbe98a0aa3a4da745b39fcd7427dc2d623055e63742062231ee18692a6f0fa
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"react@npm:19.2.4":
|
"react@npm:19.2.4":
|
||||||
version: 19.2.4
|
version: 19.2.4
|
||||||
resolution: "react@npm:19.2.4"
|
resolution: "react@npm:19.2.4"
|
||||||
@@ -6752,6 +6852,24 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"style-to-js@npm:1.1.21":
|
||||||
|
version: 1.1.21
|
||||||
|
resolution: "style-to-js@npm:1.1.21"
|
||||||
|
dependencies:
|
||||||
|
style-to-object: "npm:1.0.14"
|
||||||
|
checksum: 10c0/94231aa80f58f442c3a5ae01a21d10701e5d62f96b4b3e52eab3499077ee52df203cc0df4a1a870707f5e99470859136ea8657b782a5f4ca7934e0ffe662a588
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"style-to-object@npm:1.0.14":
|
||||||
|
version: 1.0.14
|
||||||
|
resolution: "style-to-object@npm:1.0.14"
|
||||||
|
dependencies:
|
||||||
|
inline-style-parser: "npm:0.2.7"
|
||||||
|
checksum: 10c0/854d9e9b77afc336e6d7b09348e7939f2617b34eb0895824b066d8cd1790284cb6d8b2ba36be88025b2595d715dba14b299ae76e4628a366541106f639e13679
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"styled-jsx@npm:5.1.6":
|
"styled-jsx@npm:5.1.6":
|
||||||
version: 5.1.6
|
version: 5.1.6
|
||||||
resolution: "styled-jsx@npm:5.1.6"
|
resolution: "styled-jsx@npm:5.1.6"
|
||||||
|
|||||||
Reference in New Issue
Block a user