Skip to content

Commit fdc85f5

Browse files
committed
feat: Bookmark styling
1 parent cb5c983 commit fdc85f5

File tree

3 files changed

+119
-43
lines changed

3 files changed

+119
-43
lines changed

packages/notion-to-jsx/src/components/Renderer/components/Bookmark/Bookmark.tsx

Lines changed: 65 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,78 @@
11
import React, { useState, useEffect } from 'react';
2-
import { MemoizedRichText } from '../MemoizedComponents';
32
import {
43
link,
54
card,
65
content,
6+
previewContainer,
77
previewImage,
88
title,
99
description,
1010
siteName,
11-
caption,
11+
favicon,
12+
urlText,
1213
} from './styles.css';
13-
import { RichTextItem } from '../RichText/RichTexts';
1414

1515
interface OpenGraphData {
1616
title: string;
1717
description: string;
1818
image: string;
1919
siteName: string;
20+
url: string;
21+
favicon?: string;
2022
}
2123

2224
export interface BookmarkProps {
2325
url: string;
24-
caption?: RichTextItem[];
2526
}
2627

27-
// 실제 프로덕션에서는 서버 사이드에서 처리하거나 전용 API를 사용해야 합니다
28+
// OpenGraph 데이터를 가져오는 함수
2829
const fetchOpenGraphData = async (url: string): Promise<OpenGraphData> => {
2930
try {
31+
const apiUrl = `https://api.microlink.io/?url=${encodeURIComponent(url)}`;
32+
const response = await fetch(apiUrl);
33+
const data = await response.json();
34+
35+
if (!response.ok) {
36+
throw new Error('Failed to fetch metadata');
37+
}
38+
39+
const { status, data: metaData } = data;
40+
41+
if (status !== 'success') {
42+
throw new Error('API returned error status');
43+
}
44+
3045
const parsedUrl = new URL(url);
3146
const domain = parsedUrl.hostname;
32-
const siteName = domain.split('.')[1] || domain;
3347

34-
// 임시로 더미 데이터를 반환
3548
return {
36-
title: domain,
37-
description: 'No description available',
38-
image: '',
39-
siteName: siteName,
49+
title: metaData.title || domain,
50+
description: metaData.description || 'No description available',
51+
image: metaData.image?.url || '',
52+
siteName: metaData.publisher || domain,
53+
url: metaData.url || '',
54+
favicon:
55+
metaData.logo?.url ||
56+
`https://www.google.com/s2/favicons?domain=${domain}&sz=64`,
4057
};
41-
} catch {
58+
} catch (error) {
59+
console.error('Error fetching OpenGraph data:', error);
60+
61+
const parsedUrl = new URL(url);
62+
const domain = parsedUrl.hostname;
63+
4264
return {
43-
title: url,
44-
description: 'Invalid URL',
65+
title: domain,
66+
description: 'No description available',
4567
image: '',
46-
siteName: 'Unknown',
68+
url: '',
69+
siteName: domain,
70+
favicon: `https://www.google.com/s2/favicons?domain=${domain}&sz=64`,
4771
};
4872
}
4973
};
5074

51-
const Bookmark: React.FC<BookmarkProps> = ({ url, caption }) => {
75+
const Bookmark: React.FC<BookmarkProps> = ({ url }) => {
5276
const [ogData, setOgData] = useState<OpenGraphData | null>(null);
5377
const [error, setError] = useState(false);
5478

@@ -68,28 +92,33 @@ const Bookmark: React.FC<BookmarkProps> = ({ url, caption }) => {
6892
return (
6993
<a href={url} target="_blank" rel="noopener noreferrer" className={link}>
7094
<div className={card}>
71-
{ogData?.image && (
72-
<img
73-
className={previewImage}
74-
src={ogData.image}
75-
alt={ogData.title}
76-
loading="lazy"
77-
/>
78-
)}
7995
<div className={content}>
80-
<h4 className={title}>{ogData?.title || url}</h4>
81-
{ogData?.description && (
82-
<p className={description}>{ogData.description}</p>
83-
)}
84-
{ogData?.siteName && (
85-
<div className={siteName}>{ogData.siteName}</div>
86-
)}
87-
{/* {caption && caption.length > 0 && (
88-
<div className={caption}>
89-
<MemoizedRichText richTexts={caption} />
90-
</div>
91-
)} */}
96+
<div>
97+
<h4 className={title}>{ogData?.title || url}</h4>
98+
<p className={description}>{ogData?.description || ''}</p>
99+
</div>
100+
<div className={siteName}>
101+
{ogData?.favicon && (
102+
<img src={ogData.favicon} alt="" className={favicon} />
103+
)}
104+
<span className={urlText}>{ogData?.url || ''}</span>
105+
</div>
92106
</div>
107+
{ogData?.image && (
108+
<div className={previewContainer}>
109+
<img
110+
className={previewImage}
111+
src={ogData.image}
112+
alt={ogData.title}
113+
loading="lazy"
114+
onError={(e) => {
115+
// 이미지 로드 실패 시 처리
116+
const target = e.target as HTMLImageElement;
117+
target.style.display = 'none';
118+
}}
119+
/>
120+
</div>
121+
)}
93122
</div>
94123
</a>
95124
);

packages/notion-to-jsx/src/components/Renderer/components/Bookmark/styles.css.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,47 +3,94 @@ import { vars } from '../../../../styles/theme.css';
33

44
export const link = style({
55
textDecoration: 'none',
6+
display: 'block',
7+
paddingTop: vars.spacing.xs,
8+
paddingBottom: vars.spacing.xs,
69
});
710

811
export const card = style({
12+
display: 'flex',
913
border: `1px solid ${vars.colors.border}`,
1014
borderRadius: vars.borderRadius.md,
1115
overflow: 'hidden',
1216
transition: 'box-shadow 0.2s ease',
17+
maxHeight: '8rem',
1318
':hover': {
1419
boxShadow: vars.shadows.md,
1520
},
1621
});
1722

1823
export const content = style({
1924
padding: vars.spacing.base,
25+
display: 'flex',
26+
flex: '4 1 180px',
27+
flexDirection: 'column',
28+
justifyContent: 'space-between',
29+
overflow: 'hidden',
30+
});
31+
32+
export const previewContainer = style({
33+
display: 'flex',
34+
flex: '1 1 180px',
35+
alignItems: 'center',
36+
justifyContent: 'center',
37+
maxHeight: '8rem',
38+
overflow: 'hidden',
2039
});
2140

2241
export const previewImage = style({
2342
width: '100%',
24-
height: '200px',
43+
height: '100%',
2544
objectFit: 'cover',
26-
background: vars.colors.code.background,
45+
objectPosition: 'center',
46+
borderRadius: vars.borderRadius.sm,
2747
});
2848

2949
export const title = style({
30-
padding: `0 0 ${vars.spacing.xs}`,
50+
padding: 0,
51+
paddingBottom: vars.spacing.xs,
3152
fontSize: vars.typography.fontSize.base,
53+
fontWeight: vars.typography.fontWeight.semibold,
3254
color: vars.colors.text,
55+
minHeight: '1.5rem',
56+
overflow: 'hidden',
57+
textOverflow: 'ellipsis',
58+
whiteSpace: 'nowrap',
3359
});
3460

3561
export const description = style({
3662
padding: 0,
37-
fontSize: vars.typography.fontSize.small,
63+
paddingBottom: vars.spacing.xs,
64+
fontSize: vars.typography.fontSize.xs,
3865
color: vars.colors.secondary,
66+
height: '2.25rem',
67+
overflow: 'hidden',
68+
textOverflow: 'ellipsis',
3969
display: '-webkit-box',
4070
WebkitLineClamp: 2,
4171
WebkitBoxOrient: 'vertical',
42-
overflow: 'hidden',
4372
});
4473

4574
export const siteName = style({
75+
minHeight: '1rem',
4676
paddingTop: vars.spacing.sm,
47-
fontSize: vars.typography.fontSize.small,
77+
fontSize: vars.typography.fontSize.xs,
4878
color: vars.colors.primary,
79+
display: 'flex',
80+
alignItems: 'center',
81+
gap: vars.spacing.sm,
82+
width: '100%',
83+
});
84+
85+
export const favicon = style({
86+
width: '1rem',
87+
height: '1rem',
88+
flexShrink: 0,
89+
});
90+
91+
export const urlText = style({
92+
overflow: 'hidden',
93+
whiteSpace: 'nowrap',
94+
textOverflow: 'ellipsis',
95+
maxWidth: '100%',
4996
});

packages/notion-to-jsx/src/components/Renderer/components/Code/CodeBlock.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import React, { useMemo } from 'react';
22
import { codeBlock } from './styles.css';
33
import Prism, { Grammar } from 'prismjs';
44
import { MemoizedRichText } from '../MemoizedComponents';
5+
import { RichTextItem } from '../RichText/RichTexts';
56

67
import 'prismjs/themes/prism.css';
78
import 'prismjs/components/prism-typescript';
89
import 'prismjs/components/prism-javascript';
910
import 'prismjs/components/prism-jsx';
1011
import 'prismjs/components/prism-tsx';
11-
import { RichTextItem } from '../RichText/RichTexts';
1212

1313
if (typeof window !== 'undefined') {
1414
window.Prism = Prism;

0 commit comments

Comments
 (0)