mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-05-01 06:23:31 +00:00
feat: add link rendering
This commit is contained in:
parent
d84f34b50c
commit
bd1e3ff832
@ -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
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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, {
|
||||||
|
|||||||
43
src/components/ui/link-renderer.tsx
Normal file
43
src/components/ui/link-renderer.tsx
Normal 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;
|
||||||
Loading…
x
Reference in New Issue
Block a user