react-beauty-link 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/dist/hooks/useBeautyLink.d.ts +11 -0
- package/dist/hooks/useBeautyLink.d.ts.map +1 -0
- package/dist/hooks/useBeautyLink.js +203 -0
- package/dist/hooks/useBeautyLink.js.map +1 -0
- package/dist/hooks/useLinkify.d.ts +11 -0
- package/dist/hooks/useLinkify.d.ts.map +1 -0
- package/dist/hooks/useLinkify.js +257 -0
- package/dist/hooks/useLinkify.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +237 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/fileIcons.d.ts +7 -0
- package/dist/utils/fileIcons.d.ts.map +1 -0
- package/dist/utils/fileIcons.js +56 -0
- package/dist/utils/fileIcons.js.map +1 -0
- package/dist/vite.svg +1 -0
- package/package.json +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andre Desbiens
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# react-beauty-link
|
|
2
|
+
|
|
3
|
+
A React hook that automatically converts URLs in text into beautiful, clickable links with page titles, favicons, and file type icons.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔗 **Automatic URL Detection** - Finds and converts HTTPS URLs in text
|
|
8
|
+
- 🎨 **Beautiful Link Previews** - Shows page titles instead of raw URLs
|
|
9
|
+
- 🌐 **Favicon Display** - Fetches and displays website favicons
|
|
10
|
+
- 📄 **File Type Icons** - Shows Nerd Font icons for 60+ file types (PDF, DOC, images, code, etc.)
|
|
11
|
+
- ⚙️ **Configurable** - Control how links open (new tab, new window, or same tab)
|
|
12
|
+
- 📏 **Smart Truncation** - Limits title length to 60 characters
|
|
13
|
+
- 🎯 **TypeScript Support** - Full type safety included
|
|
14
|
+
- 🚀 **Lightweight** - No heavy dependencies
|
|
15
|
+
- ⚡ **Fast** - Skips metadata fetching for file URLs
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install react-beauty-link
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or with yarn:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
yarn add react-beauty-link
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or with pnpm:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm add react-beauty-link
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Basic Example
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { useBeautyLink } from 'react-beauty-link';
|
|
41
|
+
|
|
42
|
+
function App() {
|
|
43
|
+
const text = "Check out https://react.dev for React docs!";
|
|
44
|
+
const linkedContent = useBeautyLink(text);
|
|
45
|
+
|
|
46
|
+
return <div>{linkedContent}</div>;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### With File Links
|
|
51
|
+
|
|
52
|
+
The hook automatically detects file extensions and shows appropriate icons:
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { useBeautyLink } from 'react-beauty-link';
|
|
56
|
+
|
|
57
|
+
function App() {
|
|
58
|
+
const text = `
|
|
59
|
+
Download report: https://example.com/quarterly-report.pdf
|
|
60
|
+
View code: https://github.com/user/repo/main.tsx
|
|
61
|
+
Get package: https://example.com/app.zip
|
|
62
|
+
`;
|
|
63
|
+
const linkedContent = useBeautyLink(text);
|
|
64
|
+
|
|
65
|
+
return <div>{linkedContent}</div>;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Result:
|
|
70
|
+
- 📕 `quarterly-report.pdf` (with red PDF icon)
|
|
71
|
+
- ⚛️ `main.tsx` (with React icon)
|
|
72
|
+
- 📦 `app.zip` (with archive icon)
|
|
73
|
+
|
|
74
|
+
### With Custom Target
|
|
75
|
+
|
|
76
|
+
Control how links open:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { useBeautyLink } from 'react-beauty-link';
|
|
80
|
+
|
|
81
|
+
function App() {
|
|
82
|
+
const text = "Visit https://github.com and https://npmjs.com";
|
|
83
|
+
|
|
84
|
+
// Open in new tab (default)
|
|
85
|
+
const newTabLinks = useBeautyLink(text, 'new-tab');
|
|
86
|
+
|
|
87
|
+
// Open in new window
|
|
88
|
+
const newWindowLinks = useBeautyLink(text, 'new-window');
|
|
89
|
+
|
|
90
|
+
// Open in same tab
|
|
91
|
+
const sameTabLinks = useBeautyLink(text, 'self');
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div>
|
|
95
|
+
<p>{newTabLinks}</p>
|
|
96
|
+
<p>{newWindowLinks}</p>
|
|
97
|
+
<p>{sameTabLinks}</p>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### TypeScript
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { useBeautyLink, LinkTarget } from 'react-beauty-link';
|
|
107
|
+
|
|
108
|
+
function App() {
|
|
109
|
+
const text = "Check out https://typescript.org";
|
|
110
|
+
const target: LinkTarget = 'new-tab'; // 'new-tab' | 'new-window' | 'self'
|
|
111
|
+
const linkedContent = useBeautyLink(text, target);
|
|
112
|
+
|
|
113
|
+
return <div>{linkedContent}</div>;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## API
|
|
118
|
+
|
|
119
|
+
### `useBeautyLink(text: string, target?: LinkTarget): ReactNode[]`
|
|
120
|
+
|
|
121
|
+
Converts URLs in text to clickable links with titles and favicons.
|
|
122
|
+
|
|
123
|
+
#### Parameters
|
|
124
|
+
|
|
125
|
+
- **`text`** (string, required): The text containing URLs to linkify
|
|
126
|
+
- **`target`** (LinkTarget, optional): How links should open
|
|
127
|
+
- `'new-tab'` (default): Opens in a new browser tab
|
|
128
|
+
- `'new-window'`: Opens in a new browser window (800x600)
|
|
129
|
+
- `'self'`: Opens in the same tab
|
|
130
|
+
|
|
131
|
+
#### Returns
|
|
132
|
+
|
|
133
|
+
- Array of React nodes containing text and link elements
|
|
134
|
+
|
|
135
|
+
### Types
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
type LinkTarget = 'new-tab' | 'new-window' | 'self';
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## How It Works
|
|
142
|
+
|
|
143
|
+
1. **URL Detection**: Scans text for HTTPS URLs (matches until first space)
|
|
144
|
+
2. **File Type Check**: If URL has a file extension, shows Nerd Font icon and filename
|
|
145
|
+
3. **Metadata Fetching**: For regular URLs, retrieves page title and favicon via CORS proxies
|
|
146
|
+
4. **Fallback Handling**: Uses Google's favicon service if fetching fails
|
|
147
|
+
5. **Rendering**: Creates beautiful links with icons and truncated titles
|
|
148
|
+
|
|
149
|
+
## Supported File Types
|
|
150
|
+
|
|
151
|
+
The hook supports 60+ file types with Nerd Font icons:
|
|
152
|
+
|
|
153
|
+
- **Documents**: PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, TXT
|
|
154
|
+
- **Code**: JS, TS, JSX, TSX, PY, JAVA, PHP, RB, GO, RS, HTML, CSS, JSON, MD, SQL
|
|
155
|
+
- **Media**: JPG, PNG, GIF, SVG, MP4, AVI, MOV, MP3, WAV, FLAC
|
|
156
|
+
- **Archives**: ZIP, RAR, 7Z, TAR, GZ
|
|
157
|
+
|
|
158
|
+
See [FILE_TYPE_SUPPORT.md](FILE_TYPE_SUPPORT.md) for the complete list with colors.
|
|
159
|
+
|
|
160
|
+
## Examples
|
|
161
|
+
|
|
162
|
+
### Before
|
|
163
|
+
```
|
|
164
|
+
Check out https://react.dev for React documentation
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### After
|
|
168
|
+
```
|
|
169
|
+
Check out [🌐 React] for React documentation
|
|
170
|
+
```
|
|
171
|
+
(Where [🌐 React] is a clickable link with the actual favicon and page title)
|
|
172
|
+
|
|
173
|
+
## Styling
|
|
174
|
+
|
|
175
|
+
Links are rendered with the following default styles:
|
|
176
|
+
- Color: `#646cff`
|
|
177
|
+
- Text decoration: `underline`
|
|
178
|
+
- Display: `inline-flex` with icons and text aligned
|
|
179
|
+
- Icon size: `16x16px`
|
|
180
|
+
|
|
181
|
+
You can override these styles using CSS:
|
|
182
|
+
|
|
183
|
+
```css
|
|
184
|
+
/* Target all linkified links */
|
|
185
|
+
a[target="_blank"] {
|
|
186
|
+
color: #your-color;
|
|
187
|
+
text-decoration: none;
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Browser Support
|
|
192
|
+
|
|
193
|
+
Works in all modern browsers that support:
|
|
194
|
+
- ES2020
|
|
195
|
+
- React 18+
|
|
196
|
+
- Fetch API
|
|
197
|
+
- DOMParser
|
|
198
|
+
|
|
199
|
+
## Notes
|
|
200
|
+
|
|
201
|
+
- Only detects HTTPS URLs (not HTTP)
|
|
202
|
+
- URLs are matched until the first space character
|
|
203
|
+
- Fetching metadata requires CORS proxies (included)
|
|
204
|
+
- Google Favicon Service is used as a reliable fallback
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT
|
|
209
|
+
|
|
210
|
+
## Contributing
|
|
211
|
+
|
|
212
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
213
|
+
|
|
214
|
+
## Issues
|
|
215
|
+
|
|
216
|
+
If you find a bug or have a feature request, please open an issue on [GitHub](https://github.com/yourusername/react-beauty-link/issues).
|
|
217
|
+
|
|
218
|
+
## Author
|
|
219
|
+
|
|
220
|
+
Your Name - [your.email@example.com](mailto:your.email@example.com)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type LinkTarget = 'new-tab' | 'new-window' | 'self';
|
|
3
|
+
/**
|
|
4
|
+
* Custom hook that converts HTTPS URLs in a string into clickable links
|
|
5
|
+
* with page titles and favicons
|
|
6
|
+
* @param text - The input string containing potential URLs
|
|
7
|
+
* @param target - How to open links: 'new-tab' (default), 'new-window', or 'self'
|
|
8
|
+
* @returns An array of React nodes with text and links
|
|
9
|
+
*/
|
|
10
|
+
export declare const useBeautyLink: (text: string, target?: LinkTarget, customColor?: string) => ReactNode[];
|
|
11
|
+
//# sourceMappingURL=useBeautyLink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useBeautyLink.d.ts","sourceRoot":"","sources":["../../src/hooks/useBeautyLink.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAQvC,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,YAAY,GAAG,MAAM,CAAC;AAe3D;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,EAAE,SAAQ,UAAsB,EAAE,cAAc,MAAM,KAAG,SAAS,EAkK3G,CAAC"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { FILE_TYPE_ICONS } from '../utils/fileIcons';
|
|
4
|
+
function getFileExtension(url) {
|
|
5
|
+
try {
|
|
6
|
+
const urlObj = new URL(url);
|
|
7
|
+
const pathname = urlObj.pathname;
|
|
8
|
+
const lastDot = pathname.lastIndexOf('.');
|
|
9
|
+
if (lastDot === -1 || lastDot === pathname.length - 1)
|
|
10
|
+
return null;
|
|
11
|
+
return pathname.substring(lastDot + 1).toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Custom hook that converts HTTPS URLs in a string into clickable links
|
|
19
|
+
* with page titles and favicons
|
|
20
|
+
* @param text - The input string containing potential URLs
|
|
21
|
+
* @param target - How to open links: 'new-tab' (default), 'new-window', or 'self'
|
|
22
|
+
* @returns An array of React nodes with text and links
|
|
23
|
+
*/
|
|
24
|
+
export const useBeautyLink = (text, target = 'new-tab', customColor) => {
|
|
25
|
+
const urlRegex = /(https:\/\/[^\s]+)/g;
|
|
26
|
+
const [linkMetadata, setLinkMetadata] = useState({});
|
|
27
|
+
const urls = Array.from(text.matchAll(urlRegex)).map(match => match[0]);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const fetchAllMetadata = async () => {
|
|
30
|
+
for (const url of urls) {
|
|
31
|
+
if (!linkMetadata[url]) {
|
|
32
|
+
const extension = getFileExtension(url);
|
|
33
|
+
// If it's a file URL, skip metadata fetching and use filename
|
|
34
|
+
if (extension && FILE_TYPE_ICONS[extension]) {
|
|
35
|
+
const filename = url.split('/').pop() || url;
|
|
36
|
+
setLinkMetadata(prev => ({
|
|
37
|
+
...prev,
|
|
38
|
+
[url]: {
|
|
39
|
+
title: decodeURIComponent(filename),
|
|
40
|
+
favicon: null
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
console.log('Fetching metadata for:', url);
|
|
47
|
+
const metadata = await fetchLinkMetadata(url);
|
|
48
|
+
console.log('Metadata received:', metadata);
|
|
49
|
+
setLinkMetadata(prev => ({
|
|
50
|
+
...prev,
|
|
51
|
+
[url]: metadata
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error('Failed to fetch metadata for', url, error);
|
|
56
|
+
setLinkMetadata(prev => ({
|
|
57
|
+
...prev,
|
|
58
|
+
[url]: {
|
|
59
|
+
title: url,
|
|
60
|
+
favicon: null
|
|
61
|
+
}
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
fetchAllMetadata();
|
|
68
|
+
}, [text, JSON.stringify(urls)]);
|
|
69
|
+
const parts = [];
|
|
70
|
+
let lastIndex = 0;
|
|
71
|
+
let match;
|
|
72
|
+
urlRegex.lastIndex = 0;
|
|
73
|
+
while ((match = urlRegex.exec(text)) !== null) {
|
|
74
|
+
const url = match[0];
|
|
75
|
+
const startIndex = match.index;
|
|
76
|
+
if (startIndex > lastIndex) {
|
|
77
|
+
parts.push(text.substring(lastIndex, startIndex));
|
|
78
|
+
}
|
|
79
|
+
const metadata = linkMetadata[url];
|
|
80
|
+
const displayTitle = metadata?.title
|
|
81
|
+
? (metadata.title.length > 60 ? metadata.title.substring(0, 60) + '...' : metadata.title)
|
|
82
|
+
: url;
|
|
83
|
+
const faviconUrl = metadata?.favicon;
|
|
84
|
+
const extension = getFileExtension(url);
|
|
85
|
+
const fileIcon = extension ? FILE_TYPE_ICONS[extension] : null;
|
|
86
|
+
const getTargetAttributes = () => {
|
|
87
|
+
switch (target) {
|
|
88
|
+
case 'new-window':
|
|
89
|
+
return {
|
|
90
|
+
target: '_blank',
|
|
91
|
+
rel: 'noopener noreferrer',
|
|
92
|
+
onClick: (e) => {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
window.open(url, '_blank', 'noopener,noreferrer,width=800,height=600');
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
case 'self':
|
|
98
|
+
return {
|
|
99
|
+
target: '_self'
|
|
100
|
+
};
|
|
101
|
+
case 'new-tab':
|
|
102
|
+
default:
|
|
103
|
+
return {
|
|
104
|
+
target: '_blank',
|
|
105
|
+
rel: 'noopener noreferrer'
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
parts.push(_jsxs("a", { href: url, ...getTargetAttributes(), style: {
|
|
110
|
+
color: customColor || '#646cff',
|
|
111
|
+
textDecoration: 'underline',
|
|
112
|
+
display: 'inline-flex',
|
|
113
|
+
alignItems: 'center',
|
|
114
|
+
gap: '6px'
|
|
115
|
+
}, children: [fileIcon ? (_jsx("span", { style: {
|
|
116
|
+
fontFamily: '"Symbols Nerd Font Mono", "Symbols Nerd Font", "Nerd Font", "FiraCode Nerd Font", monospace',
|
|
117
|
+
fontSize: '16px',
|
|
118
|
+
color: fileIcon.color,
|
|
119
|
+
lineHeight: 1,
|
|
120
|
+
display: 'inline-block',
|
|
121
|
+
width: '16px',
|
|
122
|
+
textAlign: 'center',
|
|
123
|
+
fontWeight: 'normal'
|
|
124
|
+
}, "aria-hidden": "true", children: fileIcon.icon })) : faviconUrl ? (_jsx("img", { src: faviconUrl, alt: "", style: { width: '16px', height: '16px' }, onError: (e) => {
|
|
125
|
+
e.target.src = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Cpath d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/%3E%3Cpath d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/%3E%3C/svg%3E';
|
|
126
|
+
} })) : (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }), _jsx("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })] })), _jsx("span", { children: displayTitle })] }, `link-${startIndex}`));
|
|
127
|
+
lastIndex = startIndex + url.length;
|
|
128
|
+
}
|
|
129
|
+
if (lastIndex < text.length) {
|
|
130
|
+
parts.push(text.substring(lastIndex));
|
|
131
|
+
}
|
|
132
|
+
return parts.length > 0 ? parts : [text];
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Fetches metadata (title and favicon) for a given URL
|
|
136
|
+
*/
|
|
137
|
+
async function fetchLinkMetadata(url) {
|
|
138
|
+
try {
|
|
139
|
+
const urlObj = new URL(url);
|
|
140
|
+
const origin = urlObj.origin;
|
|
141
|
+
const proxies = [
|
|
142
|
+
`https://api.allorigins.win/get?url=${encodeURIComponent(url)}`,
|
|
143
|
+
`https://corsproxy.io/?${encodeURIComponent(url)}`,
|
|
144
|
+
];
|
|
145
|
+
let html = '';
|
|
146
|
+
let success = false;
|
|
147
|
+
for (const proxyUrl of proxies) {
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(proxyUrl, {
|
|
150
|
+
signal: AbortSignal.timeout(10000) // 10 second timeout
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok)
|
|
153
|
+
continue;
|
|
154
|
+
const data = await response.json();
|
|
155
|
+
html = data.contents || data;
|
|
156
|
+
success = true;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
console.warn('Proxy failed:', proxyUrl, err);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (!success || !html) {
|
|
165
|
+
throw new Error('All proxies failed');
|
|
166
|
+
}
|
|
167
|
+
const parser = new DOMParser();
|
|
168
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
169
|
+
let title = doc.querySelector('meta[property="og:title"]')?.getAttribute('content') ||
|
|
170
|
+
doc.querySelector('meta[name="twitter:title"]')?.getAttribute('content') ||
|
|
171
|
+
doc.querySelector('title')?.textContent ||
|
|
172
|
+
url;
|
|
173
|
+
title = title.trim();
|
|
174
|
+
let favicon = `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=32`;
|
|
175
|
+
const faviconLink = doc.querySelector('link[rel="icon"]')?.getAttribute('href') ||
|
|
176
|
+
doc.querySelector('link[rel="shortcut icon"]')?.getAttribute('href') ||
|
|
177
|
+
doc.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href');
|
|
178
|
+
if (faviconLink) {
|
|
179
|
+
if (faviconLink.startsWith('http')) {
|
|
180
|
+
favicon = faviconLink;
|
|
181
|
+
}
|
|
182
|
+
else if (faviconLink.startsWith('//')) {
|
|
183
|
+
favicon = 'https:' + faviconLink;
|
|
184
|
+
}
|
|
185
|
+
else if (faviconLink.startsWith('/')) {
|
|
186
|
+
favicon = origin + faviconLink;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
favicon = origin + '/' + faviconLink;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { title, favicon };
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
console.error('Error fetching metadata:', error);
|
|
196
|
+
const urlObj = new URL(url);
|
|
197
|
+
return {
|
|
198
|
+
title: urlObj.hostname,
|
|
199
|
+
favicon: `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=32`
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=useBeautyLink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useBeautyLink.js","sourceRoot":"","sources":["../../src/hooks/useBeautyLink.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
export type LinkTarget = 'new-tab' | 'new-window' | 'self';
|
|
3
|
+
/**
|
|
4
|
+
* Custom hook that converts HTTPS URLs in a string into clickable links
|
|
5
|
+
* with page titles and favicons
|
|
6
|
+
* @param text - The input string containing potential URLs
|
|
7
|
+
* @param target - How to open links: 'new-tab' (default), 'new-window', or 'self'
|
|
8
|
+
* @returns An array of React nodes with text and links
|
|
9
|
+
*/
|
|
10
|
+
export declare const useLinkify: (text: string, target?: LinkTarget) => ReactNode[];
|
|
11
|
+
//# sourceMappingURL=useLinkify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLinkify.d.ts","sourceRoot":"","sources":["../../src/hooks/useLinkify.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAuB,MAAM,OAAO,CAAC;AAOvD,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,YAAY,GAAG,MAAM,CAAC;AAiF3D;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,EAAE,SAAQ,UAAsB,KAAG,SAAS,EAgKlF,CAAC"}
|