Apply UI updates: tags gap, notifications, card click, breadcrumb, move share input

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-02-15 11:10:25 -05:00
parent 8cda50ac96
commit 6be3335ada
16 changed files with 370 additions and 76 deletions

View File

@@ -1,5 +1,7 @@
import React, { useState, startTransition } from 'react'
import useAppStore from '../store/useAppStore'
import { generateRandomMarkdown } from '../utils/fileHelpers'
import MarkdownPreview from './MarkdownPreview'
const CreatePostModal: React.FC = () => {
const isOpen = useAppStore((s) => s.ui.isCreatePostOpen)
@@ -8,28 +10,32 @@ const CreatePostModal: React.FC = () => {
const createPost = useAppStore((s) => s.createPost)
const [content, setContent] = useState('')
const [file, setFile] = useState<File | null>(null)
const [attachedMarkdown, setAttachedMarkdown] = useState<{name:string,content:string} | null>(null)
const [error, setError] = useState<string | null>(null)
if (!isOpen) return null
const onGenerate = () => {
const md = generateRandomMarkdown()
setAttachedMarkdown(md)
}
const onSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!currentUserId) {
setError('Select a user first')
return
}
if (!content.trim()) {
setError('Content required')
if (!content.trim() && !attachedMarkdown) {
setError('Content or markdown required')
return
}
setError(null)
startTransition(() => {
const attachedPDF = file ? { name: file.name, url: URL.createObjectURL(file) } : undefined
createPost({ authorId: currentUserId, content: content.trim(), attachedPDF })
createPost({ authorId: currentUserId, content: content.trim(), attachedMarkdown: attachedMarkdown ?? undefined })
toggle()
setContent('')
setFile(null)
setAttachedMarkdown(null)
})
}
@@ -40,14 +46,8 @@ const CreatePostModal: React.FC = () => {
<form onSubmit={onSubmit}>
<textarea className="textarea" value={content} onChange={(e) => setContent(e.target.value)} />
<div style={{marginTop:8}}>
<input type="file" accept="application/pdf" onChange={(e) => {
const f = e.target.files && e.target.files[0]
if (f && f.type !== 'application/pdf') {
setError('Only PDF files allowed')
return
}
setFile(f ?? null)
}} />
<button type="button" className="button" onClick={onGenerate}>Generate Markdown</button>
{attachedMarkdown && <div style={{marginTop:8}}><MarkdownPreview content={attachedMarkdown.content} /></div>}
</div>
{error && <div style={{color:'red'}}>{error}</div>}
<div style={{marginTop:8,display:'flex',justifyContent:'flex-end',gap:8}}>

View File

@@ -0,0 +1,27 @@
import React from 'react'
import useAppStore from '../store/useAppStore'
import MarkdownPreview from './MarkdownPreview'
const MarkdownModal: React.FC = () => {
const selectedPostId = useAppStore((s) => s.selectedPostId)
const setSelectedPost = useAppStore((s) => s.setSelectedPost)
const post = useAppStore((s) => s.posts.find((p) => p.id === selectedPostId))
if (!selectedPostId || !post?.attachedMarkdown) return null
return (
<div className="modal-backdrop" onClick={() => setSelectedPost(null)}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center'}}>
<h3>{post.attachedMarkdown.name}</h3>
<button onClick={() => setSelectedPost(null)}>Close</button>
</div>
<div style={{marginTop:8}}>
<MarkdownPreview content={post.attachedMarkdown.content} />
</div>
</div>
</div>
)
}
export default MarkdownModal

View File

@@ -0,0 +1,32 @@
import React, { useMemo } from 'react'
const renderMarkdown = (md: string) => {
if (!md) return ''
let html = md
// code blocks
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
// headings
html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>')
html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>')
html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>')
// bold / italics
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>')
// links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>')
// lists
html = html.replace(/^\s*-\s+(.*)$/gim, '<li>$1</li>')
// wrap list items
html = html.replace(/(<li>[\s\S]*?<\/li>)/gms, '<ul>$1</ul>')
// paragraphs
html = html.replace(/\n{2,}/g, '</p><p>')
html = `<p>${html}</p>`
return html
}
const MarkdownPreview: React.FC<{ content: string }> = ({ content }) => {
const html = useMemo(() => renderMarkdown(content), [content])
return <div className="markdown-preview" dangerouslySetInnerHTML={{ __html: html }} />
}
export default MarkdownPreview

View File

@@ -0,0 +1,22 @@
import React from 'react'
import useAppStore from '../store/useAppStore'
const NotificationCenter: React.FC = () => {
const notifications = useAppStore((s) => s.notifications)
const removeNotification = useAppStore((s) => s.removeNotification)
if (!notifications || notifications.length === 0) return null
return (
<div className="notif-container">
{notifications.map((n) => (
<div key={n.id} className={`notif ${n.type ?? ''}`}>
<div>{n.message}</div>
<button onClick={() => removeNotification(n.id)}>×</button>
</div>
))}
</div>
)
}
export default NotificationCenter

View File

@@ -1,46 +1,106 @@
import React, { memo } from 'react'
import React, { memo, useState } from 'react'
import { Post } from '../types/Post'
import useAppStore from '../store/useAppStore'
import { Link } from 'react-router-dom'
import { formatTime } from '../utils/fileHelpers'
import PDFPreview from './PDFPreview'
import { Link, useNavigate } from 'react-router-dom'
import { formatTime, generateToken } from '../utils/fileHelpers'
import MarkdownPreview from './MarkdownPreview'
const PostCard: React.FC<{ post: Post }> = ({ post }) => {
const author = useAppStore((s) => s.users.find((u) => u.id === post.authorId))
const endorsePost = useAppStore((s) => s.endorsePost)
const setSelectedPost = useAppStore((s) => s.setSelectedPost)
const currentUserId = useAppStore((s) => s.currentUserId)
const addNotification = useAppStore((s) => s.addNotification)
const navigate = useNavigate()
const [shareUrl, setShareUrl] = useState<string | null>(null)
const [copied, setCopied] = useState(false)
if (!author) return null
const handleEndorse = async (e?: React.MouseEvent) => {
e?.stopPropagation()
if (!currentUserId) {
addNotification('Select a current user (top-right) to endorse from.', 'error')
return
}
if (currentUserId === post.authorId) {
addNotification("You can't endorse your own post.", 'error')
return
}
endorsePost(post.id)
const token = generateToken(6)
const url = `https://arxiv.org/auth/endorse?x=${token}`
setShareUrl(url)
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(url)
setCopied(true)
addNotification('Share link copied to clipboard', 'success')
} else {
const area = document.createElement('textarea')
area.value = url
document.body.appendChild(area)
area.select()
document.execCommand('copy')
document.body.removeChild(area)
setCopied(true)
addNotification('Share link copied to clipboard', 'success')
}
} catch (e) {
console.error('copy failed', e)
addNotification('Could not copy share link', 'error')
}
setTimeout(() => setCopied(false), 2000)
}
const handleCopy = async (e?: React.MouseEvent) => {
e?.stopPropagation()
if (!shareUrl) return
try {
await navigator.clipboard.writeText(shareUrl)
setCopied(true)
addNotification('Share link copied to clipboard', 'success')
setTimeout(() => setCopied(false), 2000)
} catch {
addNotification('Could not copy share link', 'error')
}
}
return (
<div className="card">
<div className="card post-card" onClick={() => navigate(`/post/${post.id}`)} style={{cursor:'pointer'}}>
<div style={{display:'flex',justifyContent:'space-between'}}>
<div>
<Link to={`/profile/${author.id}`}><strong>{author.name}</strong></Link>
<Link to={`/profile/${author.id}`} onClick={(e) => e.stopPropagation()}><strong>{author.name}</strong></Link>
<div className="small">{formatTime(post.createdAt)} ago</div>
</div>
<div style={{display:'flex',gap:6}}>
<div className="tags-row">
{author.specialties.map((sp) => (
<span key={sp} className="tag">{sp}</span>
))}
</div>
</div>
<p style={{marginTop:8}}>{post.content}</p>
{post.attachedPDF && (
{post.attachedMarkdown && (
<div style={{marginTop:8}}>
<div className="small">{post.attachedPDF.name}</div>
<div style={{marginTop:6,display:'flex',gap:8}}>
<button className="button" onClick={() => setSelectedPost(post.id)}>Open PDF</button>
<PDFPreview url={post.attachedPDF.url} name={post.attachedPDF.name} />
<div className="small">{post.attachedMarkdown.name}</div>
<div style={{marginTop:6}}>
<MarkdownPreview content={post.attachedMarkdown.content} />
</div>
</div>
)}
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginTop:8}}>
<div>
<button className="button" onClick={() => endorsePost(post.id)}>Endorse Post ({post.endorsements})</button>
<Link to={`/post/${post.id}`} style={{marginLeft:8}}>View</Link>
<button className="button" onClick={(e) => { e.stopPropagation(); handleEndorse(e); }}>Endorse Post ({post.endorsements})</button>
<button className="button" style={{marginLeft:8}} onClick={(e) => { e.stopPropagation(); setSelectedPost(post.id); }}>Open MD</button>
</div>
</div>
{shareUrl && (
<div className="small" style={{marginTop:8}}>
Share link: <a href={shareUrl} target="_blank" rel="noreferrer" onClick={(e) => e.stopPropagation()}>{shareUrl}</a>
<button style={{marginLeft:8}} onClick={(e) => { e.stopPropagation(); handleCopy(e); }}>{copied ? 'Copied' : 'Copy'}</button>
</div>
)}
</div>
)
}

View File

@@ -1,13 +1,34 @@
import React from 'react'
import { User } from '../types/User'
import { Link } from 'react-router-dom'
import useAppStore from '../store/useAppStore'
const UserCard: React.FC<{ user: User }> = ({ user }) => {
const currentUserId = useAppStore((s) => s.currentUserId)
const endorseUser = useAppStore((s) => s.endorseUser)
const addNotification = useAppStore((s) => s.addNotification)
const onEmailClick = (e: React.MouseEvent) => {
e.preventDefault()
if (!currentUserId) {
addNotification('Select a current user (top-right) to endorse from.', 'error')
return
}
if (currentUserId === user.id) {
addNotification("You can't endorse yourself.", 'error')
return
}
const specialty = user.specialties[0] ?? 'General'
endorseUser(user.id, specialty)
addNotification(`Endorsed ${user.name} for ${specialty}`, 'success')
}
return (
<div className="card">
<Link to={`/profile/${user.id}`}><strong>{user.name}</strong></Link>
<div className="small"><a href="#" onClick={onEmailClick}>{user.email}</a></div>
<div className="small">{user.bio}</div>
<div style={{marginTop:8}}>
<div className="tags-row" style={{marginTop:8}}>
{user.specialties.map((s) => (
<span key={s} className="tag">{s} ({user.endorsements[s] ?? 0})</span>
))}