feat: scaffold initial RSS reader project with Go backend and React frontend
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import React from 'react';
|
||||
import { FeedEntry } from '../types';
|
||||
import { estimateReadingTime } from '../utils';
|
||||
import {
|
||||
X,
|
||||
Star,
|
||||
CheckCircle2,
|
||||
ExternalLink,
|
||||
Share2,
|
||||
Clock,
|
||||
User as UserIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface ArticleModalProps {
|
||||
entry: FeedEntry;
|
||||
onClose: () => void;
|
||||
onToggleStar: (e?: React.MouseEvent) => void;
|
||||
onToggleRead: (e?: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
export const ArticleModal: React.FC<ArticleModalProps> = ({
|
||||
entry,
|
||||
onClose,
|
||||
onToggleStar,
|
||||
onToggleRead,
|
||||
}) => {
|
||||
const shareUrl = `${window.location.origin}/share/${entry.share_uuid || entry.id}.html`;
|
||||
|
||||
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||
if (entry.is_read) return;
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
|
||||
if (scrollHeight <= clientHeight + 40) return;
|
||||
const scrollProgress = (scrollTop + clientHeight) / scrollHeight;
|
||||
if (scrollProgress >= 0.9) {
|
||||
onToggleRead();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="article-drawer-overlay" onClick={onClose}>
|
||||
<div
|
||||
className="article-drawer-card"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Top Header Actions */}
|
||||
<div className="article-drawer-header">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<button
|
||||
className="btn-icon"
|
||||
style={{
|
||||
color: entry.is_starred ? 'var(--star)' : undefined,
|
||||
}}
|
||||
onClick={onToggleStar}
|
||||
title={entry.is_starred ? 'Unstar (S)' : 'Star (S)'}
|
||||
>
|
||||
<Star
|
||||
size={17}
|
||||
fill={entry.is_starred ? 'var(--star)' : 'none'}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn-icon"
|
||||
style={{
|
||||
color: entry.is_read ? 'var(--success)' : undefined,
|
||||
}}
|
||||
onClick={onToggleRead}
|
||||
title={entry.is_read ? 'Mark Unread' : 'Mark Read'}
|
||||
>
|
||||
<CheckCircle2 size={17} />
|
||||
</button>
|
||||
|
||||
<span style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginLeft: '0.5rem' }}>
|
||||
{entry.feed_title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<a
|
||||
href={shareUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-secondary"
|
||||
style={{ fontSize: '0.75rem', padding: '0.3rem 0.6rem' }}
|
||||
>
|
||||
<Share2 size={13} />
|
||||
<span>Share</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={entry.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-secondary"
|
||||
style={{ fontSize: '0.75rem', padding: '0.3rem 0.6rem' }}
|
||||
>
|
||||
<ExternalLink size={13} />
|
||||
<span>Original Link</span>
|
||||
</a>
|
||||
|
||||
<button className="btn-icon" onClick={onClose}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Article Scroll Body */}
|
||||
<div className="article-drawer-scroll" onScroll={handleScroll}>
|
||||
<div className="reader-canvas" style={{ padding: 0 }}>
|
||||
<div className="reader-source-bar">
|
||||
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>
|
||||
{entry.feed_title}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Clock size={13} />{' '}
|
||||
{new Date(entry.published_at).toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>{estimateReadingTime(entry.content || entry.raw_content)}</span>
|
||||
{entry.author && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<UserIcon size={13} /> {entry.author}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className="reader-headline">
|
||||
<a href={entry.url} target="_blank" rel="noopener noreferrer">
|
||||
{entry.title}
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<div
|
||||
className="reader-body"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: entry.content || entry.raw_content || '',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user