feat: add link rendering

This commit is contained in:
Danish Arora 2025-09-10 15:04:24 +05:30
parent d84f34b50c
commit bd1e3ff832
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
6 changed files with 57 additions and 5 deletions

View File

@ -5,6 +5,7 @@ import { formatDistanceToNow } from 'date-fns';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { MessageSquareText, Newspaper } from 'lucide-react'; import { MessageSquareText, Newspaper } from 'lucide-react';
import { AuthorDisplay } from './ui/author-display'; import { AuthorDisplay } from './ui/author-display';
import { LinkRenderer } from './ui/link-renderer';
interface FeedItemBase { interface FeedItemBase {
id: string; id: string;
@ -135,7 +136,7 @@ const ActivityFeed: React.FC = () => {
) : ( ) : (
<div> <div>
<div className="text-sm line-clamp-3 mb-1"> <div className="text-sm line-clamp-3 mb-1">
{item.content} <LinkRenderer text={item.content} />
</div> </div>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
{item.voteCount} Reply to post {item.voteCount} Reply to post

View File

@ -11,6 +11,7 @@ import {
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { BookmarkButton } from '@/components/ui/bookmark-button'; import { BookmarkButton } from '@/components/ui/bookmark-button';
import { AuthorDisplay } from '@/components/ui/author-display'; import { AuthorDisplay } from '@/components/ui/author-display';
import { LinkRenderer } from '@/components/ui/link-renderer';
import { usePending, usePendingVote } from '@/hooks/usePending'; import { usePending, usePendingVote } from '@/hooks/usePending';
import { import {
Tooltip, Tooltip,
@ -132,7 +133,9 @@ const CommentCard: React.FC<CommentCardProps> = ({
/> />
</div> </div>
<p className="text-sm break-words mb-2">{comment.content}</p> <p className="text-sm break-words mb-2">
<LinkRenderer text={comment.content} />
</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{canModerate && !comment.moderated && ( {canModerate && !comment.moderated && (

View File

@ -13,6 +13,7 @@ import {
import { RelevanceIndicator } from '@/components/ui/relevance-indicator'; import { RelevanceIndicator } from '@/components/ui/relevance-indicator';
import { AuthorDisplay } from '@/components/ui/author-display'; import { AuthorDisplay } from '@/components/ui/author-display';
import { BookmarkButton } from '@/components/ui/bookmark-button'; import { BookmarkButton } from '@/components/ui/bookmark-button';
import { LinkRenderer } from '@/components/ui/link-renderer';
import { usePending, usePendingVote } from '@/hooks/usePending'; import { usePending, usePendingVote } from '@/hooks/usePending';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
@ -187,7 +188,7 @@ const PostCard: React.FC<PostCardProps> = ({ post, commentCount = 0 }) => {
{/* Post content preview */} {/* Post content preview */}
<p className="text-cyber-neutral text-sm leading-relaxed mb-3"> <p className="text-cyber-neutral text-sm leading-relaxed mb-3">
{contentPreview} <LinkRenderer text={contentPreview} />
</p> </p>
{/* Post actions */} {/* Post actions */}

View File

@ -24,6 +24,7 @@ import { formatDistanceToNow } from 'date-fns';
import { RelevanceIndicator } from './ui/relevance-indicator'; import { RelevanceIndicator } from './ui/relevance-indicator';
import { AuthorDisplay } from './ui/author-display'; import { AuthorDisplay } from './ui/author-display';
import { BookmarkButton } from './ui/bookmark-button'; import { BookmarkButton } from './ui/bookmark-button';
import { LinkRenderer } from './ui/link-renderer';
import CommentCard from './CommentCard'; import CommentCard from './CommentCard';
import { usePending, usePendingVote } from '@/hooks/usePending'; import { usePending, usePendingVote } from '@/hooks/usePending';
@ -236,7 +237,7 @@ const PostDetail = () => {
/> />
</div> </div>
<p className="text-sm whitespace-pre-wrap break-words"> <p className="text-sm whitespace-pre-wrap break-words">
{post.content} <LinkRenderer text={post.content} />
</p> </p>
</div> </div>
</div> </div>

View File

@ -13,6 +13,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { LinkRenderer } from '@/components/ui/link-renderer';
import { import {
ArrowLeft, ArrowLeft,
MessageSquare, MessageSquare,
@ -300,7 +301,9 @@ const PostList = () => {
<h2 className="text-lg font-bold hover:text-cyber-accent"> <h2 className="text-lg font-bold hover:text-cyber-accent">
{post.title} {post.title}
</h2> </h2>
<p className="line-clamp-2 text-sm mb-3">{post.content}</p> <p className="line-clamp-2 text-sm mb-3">
<LinkRenderer text={post.content} />
</p>
<div className="flex items-center gap-4 text-xs text-cyber-neutral"> <div className="flex items-center gap-4 text-xs text-cyber-neutral">
<span> <span>
{formatDistanceToNow(post.timestamp, { {formatDistanceToNow(post.timestamp, {

View File

@ -0,0 +1,43 @@
import React from 'react';
interface LinkRendererProps {
text: string;
className?: string;
}
/**
* Component that renders text with clickable links
* Detects URLs and converts them to clickable <a> tags
*/
export const LinkRenderer: React.FC<LinkRendererProps> = ({ text, className }) => {
// URL regex pattern that matches http/https URLs
const urlRegex = /(https?:\/\/[^\s]+)/g;
// Split text by URLs and create array of text segments and URLs
const parts = text.split(urlRegex);
return (
<span className={className}>
{parts.map((part, index) => {
// Check if this part is a URL
if (urlRegex.test(part)) {
return (
<a
key={index}
href={part}
target="_blank"
rel="noopener noreferrer"
className="text-cyber-accent hover:text-cyber-accent/80 underline transition-colors"
>
{part}
</a>
);
}
// Regular text
return part;
})}
</span>
);
};
export default LinkRenderer;