tududi/frontend/components/Shared/UrlPreview.tsx
Chris 03f38f05dc
Setup intelligence (#84)
* Add next suggestions and remove console logs

* Add pomodoro timer

* Add pomodoro switch in settings

* Fix pomodoro setting

* Add timezones to settings

* Fix an issue with password reset

* Cleanup

* Sort tags alphabetically

* Clean up today's view

* Add an indicator for repeatedly added to today

* Refactor tags

* Add due date today item

* Move recurrence to the subtitle area

* Fix today layout

* Add a badge to Inbox items

* Move inbox badge to sidebar

* Add quotes and progress bar

* Add translations for quotes

* Fix test issues

* Add helper script for docker local

* Set up overdue tasks

* Add  linux/arm/v7 build to deploy script

* Add  linux/arm/v7 build to deploy script pt2

* Fix an issue with helmet and SSL

* Add volume db persistence

* Fix cog icon issues
2025-06-27 14:02:18 +03:00

124 lines
No EOL
3.8 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { extractTitleFromText, UrlTitleResult } from '../../utils/urlService';
import { LinkIcon, XMarkIcon, PhotoIcon } from '@heroicons/react/24/outline';
interface UrlPreviewProps {
text: string;
onPreviewChange?: (preview: UrlTitleResult | null) => void;
}
const UrlPreview: React.FC<UrlPreviewProps> = ({ text, onPreviewChange }) => {
const [preview, setPreview] = useState<UrlTitleResult | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isVisible, setIsVisible] = useState(true);
const [imageError, setImageError] = useState(false);
useEffect(() => {
const extractPreview = async () => {
if (!text.trim()) {
setPreview(null);
onPreviewChange?.(null);
return;
}
setIsLoading(true);
try {
const result = await extractTitleFromText(text);
setPreview(result);
onPreviewChange?.(result);
} catch (error) {
console.error('Failed to extract URL preview:', error);
setPreview(null);
onPreviewChange?.(null);
} finally {
setIsLoading(false);
}
};
const timeoutId = setTimeout(extractPreview, 300);
return () => clearTimeout(timeoutId);
}, [text, onPreviewChange]);
const handleDismiss = () => {
setIsVisible(false);
setPreview(null);
onPreviewChange?.(null);
};
const handleImageError = () => {
setImageError(true);
};
if (!isVisible || (!preview && !isLoading)) {
return null;
}
if (isLoading) {
return (
<div className="mt-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600">
<div className="flex items-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500"></div>
<span className="text-sm text-gray-600 dark:text-gray-300">Loading preview...</span>
</div>
</div>
);
}
if (!preview) {
return null;
}
return (
<div className="mt-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600 relative">
<button
onClick={handleDismiss}
className="absolute top-2 right-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 z-10"
aria-label="Dismiss preview"
>
<XMarkIcon className="h-4 w-4" />
</button>
<div className="flex items-start space-x-3">
<div className="flex-shrink-0">
{preview.image && !imageError ? (
<img
src={preview.image}
alt="Preview"
className="w-16 h-16 object-cover rounded-md"
onError={handleImageError}
/>
) : (
<div className="w-16 h-16 bg-gray-200 dark:bg-gray-600 rounded-md flex items-center justify-center">
<PhotoIcon className="h-8 w-8 text-gray-400" />
</div>
)}
</div>
<div className="flex-1 min-w-0 pr-6">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100" style={{
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden'
}}>
{preview.title || 'Untitled'}
</div>
{preview.description && (
<div className="text-xs text-gray-600 dark:text-gray-300 mt-1" style={{
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden'
}}>
{preview.description}
</div>
)}
<div className="text-xs text-gray-500 dark:text-gray-400 truncate mt-1">
{preview.url}
</div>
</div>
</div>
</div>
);
};
export default UrlPreview;