Save workspace changes
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,63 +1,84 @@
|
||||
import React, { useState, startTransition } from 'react'
|
||||
import useAppStore from '../store/useAppStore'
|
||||
import { generateRandomMarkdown } from '../utils/fileHelpers'
|
||||
import MarkdownPreview from './MarkdownPreview'
|
||||
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)
|
||||
const toggle = useAppStore((s) => s.toggleCreatePost)
|
||||
const currentUserId = useAppStore((s) => s.currentUserId)
|
||||
const createPost = useAppStore((s) => s.createPost)
|
||||
const isOpen = useAppStore((s) => s.ui.isCreatePostOpen);
|
||||
const toggle = useAppStore((s) => s.toggleCreatePost);
|
||||
const currentUserId = useAppStore((s) => s.currentUserId);
|
||||
const createPost = useAppStore((s) => s.createPost);
|
||||
|
||||
const [content, setContent] = useState('')
|
||||
const [attachedMarkdown, setAttachedMarkdown] = useState<{name:string,content:string} | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [content, setContent] = useState('');
|
||||
const [attachedMarkdown, setAttachedMarkdown] = useState<{
|
||||
name: string;
|
||||
content: string;
|
||||
} | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
if (!isOpen) return null
|
||||
if (!isOpen) return null;
|
||||
|
||||
const onGenerate = () => {
|
||||
const md = generateRandomMarkdown()
|
||||
setAttachedMarkdown(md)
|
||||
}
|
||||
const md = generateRandomMarkdown();
|
||||
setAttachedMarkdown(md);
|
||||
};
|
||||
|
||||
const onSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
e.preventDefault();
|
||||
if (!currentUserId) {
|
||||
setError('Select a user first')
|
||||
return
|
||||
setError('Select a user first');
|
||||
return;
|
||||
}
|
||||
if (!content.trim() && !attachedMarkdown) {
|
||||
setError('Content or markdown required')
|
||||
return
|
||||
setError('Content or markdown required');
|
||||
return;
|
||||
}
|
||||
setError(null)
|
||||
setError(null);
|
||||
startTransition(() => {
|
||||
createPost({ authorId: currentUserId, content: content.trim(), attachedMarkdown: attachedMarkdown ?? undefined })
|
||||
toggle()
|
||||
setContent('')
|
||||
setAttachedMarkdown(null)
|
||||
})
|
||||
}
|
||||
createPost({
|
||||
authorId: currentUserId,
|
||||
content: content.trim(),
|
||||
attachedMarkdown: attachedMarkdown ?? undefined,
|
||||
});
|
||||
toggle();
|
||||
setContent('');
|
||||
setAttachedMarkdown(null);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop">
|
||||
<div className="modal">
|
||||
<h3>Create Post</h3>
|
||||
<form onSubmit={onSubmit}>
|
||||
<textarea className="textarea" value={content} onChange={(e) => setContent(e.target.value)} />
|
||||
<div style={{marginTop:8}}>
|
||||
<button type="button" className="button" onClick={onGenerate}>Generate Markdown</button>
|
||||
{attachedMarkdown && <div style={{marginTop:8}}><MarkdownPreview content={attachedMarkdown.content} /></div>}
|
||||
<textarea
|
||||
className="textarea"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
/>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<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}}>
|
||||
<button type="button" onClick={() => toggle()}>Cancel</button>
|
||||
<button className="button" type="submit" disabled={!currentUserId}>Post</button>
|
||||
{error && <div style={{ color: 'red' }}>{error}</div>}
|
||||
<div style={{ marginTop: 8, display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
<button type="button" onClick={() => toggle()}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="button" type="submit" disabled={!currentUserId}>
|
||||
Post
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePostModal
|
||||
export default CreatePostModal;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import React from 'react'
|
||||
import React from 'react';
|
||||
|
||||
const EndorseButton: React.FC<{ onClick: () => void; count: number; disabled?: boolean }> = ({ onClick, count, disabled }) => {
|
||||
const EndorseButton: React.FC<{ onClick: () => void; count: number; disabled?: boolean }> = ({
|
||||
onClick,
|
||||
count,
|
||||
disabled,
|
||||
}) => {
|
||||
return (
|
||||
<button className="button" onClick={onClick} disabled={disabled}>
|
||||
Endorse ({count})
|
||||
</button>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default EndorseButton
|
||||
export default EndorseButton;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React from 'react'
|
||||
import useAppStore from '../store/useAppStore'
|
||||
import PostCard from './PostCard'
|
||||
import React from 'react';
|
||||
import useAppStore from '../store/useAppStore';
|
||||
import PostCard from './PostCard';
|
||||
|
||||
const Feed: React.FC = () => {
|
||||
const posts = useAppStore((s) => s.posts)
|
||||
const sorted = [...posts].sort((a,b) => b.createdAt - a.createdAt)
|
||||
if (sorted.length === 0) return <div className="card">No posts yet.</div>
|
||||
const posts = useAppStore((s) => s.posts);
|
||||
const sorted = [...posts].sort((a, b) => b.createdAt - a.createdAt);
|
||||
if (sorted.length === 0) return <div className="card">No posts yet.</div>;
|
||||
return (
|
||||
<div>
|
||||
{sorted.map((p) => (
|
||||
<PostCard key={p.id} post={p} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Feed
|
||||
export default Feed;
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import React from 'react'
|
||||
import useAppStore from '../store/useAppStore'
|
||||
import MarkdownPreview from './MarkdownPreview'
|
||||
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))
|
||||
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
|
||||
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'}}>
|
||||
<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}}>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<MarkdownPreview content={post.attachedMarkdown.content} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownModal
|
||||
export default MarkdownModal;
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
const renderMarkdown = (md: string) => {
|
||||
if (!md) return ''
|
||||
let html = md
|
||||
if (!md) return '';
|
||||
let html = md;
|
||||
// code blocks
|
||||
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
|
||||
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>')
|
||||
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>')
|
||||
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>')
|
||||
html = html.replace(
|
||||
/\[([^\]]+)\]\(([^)]+)\)/g,
|
||||
'<a href="$2" target="_blank" rel="noreferrer">$1</a>'
|
||||
);
|
||||
// lists
|
||||
html = html.replace(/^\s*-\s+(.*)$/gim, '<li>$1</li>')
|
||||
html = html.replace(/^\s*-\s+(.*)$/gim, '<li>$1</li>');
|
||||
// wrap list items
|
||||
html = html.replace(/(<li>[\s\S]*?<\/li>)/gms, '<ul>$1</ul>')
|
||||
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
|
||||
}
|
||||
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 }} />
|
||||
}
|
||||
const html = useMemo(() => renderMarkdown(content), [content]);
|
||||
return <div className="markdown-preview" dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
};
|
||||
|
||||
export default MarkdownPreview
|
||||
export default MarkdownPreview;
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import useAppStore from '../store/useAppStore'
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import useAppStore from '../store/useAppStore';
|
||||
|
||||
const Navbar: React.FC = () => {
|
||||
const users = useAppStore((s) => s.users)
|
||||
const currentUserId = useAppStore((s) => s.currentUserId)
|
||||
const setCurrentUser = useAppStore((s) => s.setCurrentUser)
|
||||
const toggleCreatePost = useAppStore((s) => s.toggleCreatePost)
|
||||
const users = useAppStore((s) => s.users);
|
||||
const currentUserId = useAppStore((s) => s.currentUserId);
|
||||
const setCurrentUser = useAppStore((s) => s.setCurrentUser);
|
||||
const toggleCreatePost = useAppStore((s) => s.toggleCreatePost);
|
||||
|
||||
return (
|
||||
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between'}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Link to="/">Researcher Endorsement</Link>
|
||||
<Link to="/create-user" style={{marginLeft:12}}>Create User</Link>
|
||||
<Link to="/create-user" style={{ marginLeft: 12 }}>
|
||||
Create User
|
||||
</Link>
|
||||
</div>
|
||||
<div style={{display:'flex',alignItems:'center',gap:12}}>
|
||||
<select value={currentUserId ?? ''} onChange={(e) => setCurrentUser(e.target.value || null)}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<select
|
||||
value={currentUserId ?? ''}
|
||||
onChange={(e) => setCurrentUser(e.target.value || null)}
|
||||
>
|
||||
<option value="">(No user)</option>
|
||||
{users.map((u) => (
|
||||
<option key={u.id} value={u.id}>{u.name}</option>
|
||||
<option key={u.id} value={u.id}>
|
||||
{u.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button className="button" onClick={() => toggleCreatePost()} disabled={!currentUserId}>Create Post</button>
|
||||
<button className="button" onClick={() => toggleCreatePost()} disabled={!currentUserId}>
|
||||
Create Post
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar
|
||||
export default Navbar;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react'
|
||||
import useAppStore from '../store/useAppStore'
|
||||
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)
|
||||
const notifications = useAppStore((s) => s.notifications);
|
||||
const removeNotification = useAppStore((s) => s.removeNotification);
|
||||
|
||||
if (!notifications || notifications.length === 0) return null
|
||||
if (!notifications || notifications.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="notif-container">
|
||||
@@ -16,7 +16,7 @@ const NotificationCenter: React.FC = () => {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationCenter
|
||||
export default NotificationCenter;
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import React from 'react'
|
||||
import useAppStore from '../store/useAppStore'
|
||||
import PDFPreview from './PDFPreview'
|
||||
import React from 'react';
|
||||
import useAppStore from '../store/useAppStore';
|
||||
import PDFPreview from './PDFPreview';
|
||||
|
||||
const PDFModal: 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))
|
||||
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?.attachedPDF) return null
|
||||
if (!selectedPostId || !post?.attachedPDF) 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'}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h3>{post.attachedPDF.name}</h3>
|
||||
<button onClick={() => setSelectedPost(null)}>Close</button>
|
||||
</div>
|
||||
<div style={{marginTop:8}}>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<PDFPreview url={post.attachedPDF.url} name={post.attachedPDF.name} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PDFModal
|
||||
export default PDFModal;
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
const PDFPreview: React.FC<{ url: string; name?: string }> = ({ url, name }) => {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// only revoke blob URLs
|
||||
try {
|
||||
if (url && url.startsWith('blob:')) URL.revokeObjectURL(url)
|
||||
if (url && url.startsWith('blob:')) URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}, [url])
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="small">{name}</div>
|
||||
<iframe src={url} width="100%" height={300} title={name ?? 'pdf'} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PDFPreview
|
||||
export default PDFPreview;
|
||||
|
||||
@@ -1,108 +1,151 @@
|
||||
import React, { memo, useState } from 'react'
|
||||
import { Post } from '../types/Post'
|
||||
import useAppStore from '../store/useAppStore'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { formatTime, generateToken } from '../utils/fileHelpers'
|
||||
import MarkdownPreview from './MarkdownPreview'
|
||||
import React, { memo, useState } from 'react';
|
||||
import { Post } from '../types/Post';
|
||||
import useAppStore from '../store/useAppStore';
|
||||
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 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)
|
||||
const [shareUrl, setShareUrl] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
if (!author) return null
|
||||
if (!author) return null;
|
||||
|
||||
const handleEndorse = async (e?: React.MouseEvent) => {
|
||||
e?.stopPropagation()
|
||||
e?.stopPropagation();
|
||||
if (!currentUserId) {
|
||||
addNotification('Select a current user (top-right) to endorse from.', 'error')
|
||||
return
|
||||
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
|
||||
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)
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
console.error('copy failed', e);
|
||||
addNotification('Could not copy share link', 'error');
|
||||
}
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
const handleCopy = async (e?: React.MouseEvent) => {
|
||||
e?.stopPropagation()
|
||||
if (!shareUrl) return
|
||||
e?.stopPropagation();
|
||||
if (!shareUrl) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl)
|
||||
setCopied(true)
|
||||
addNotification('Share link copied to clipboard', 'success')
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
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')
|
||||
addNotification('Could not copy share link', 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card post-card" onClick={() => navigate(`/post/${post.id}`)} style={{cursor:'pointer'}}>
|
||||
<div style={{display:'flex',justifyContent:'space-between'}}>
|
||||
<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}`} onClick={(e) => e.stopPropagation()}><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 className="tags-row">
|
||||
{author.specialties.map((sp) => (
|
||||
<span key={sp} className="tag">{sp}</span>
|
||||
<span key={sp} className="tag">
|
||||
{sp}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p style={{marginTop:8}}>{post.content}</p>
|
||||
<p style={{ marginTop: 8 }}>{post.content}</p>
|
||||
{post.attachedMarkdown && (
|
||||
<div style={{marginTop:8}}>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<div className="small">{post.attachedMarkdown.name}</div>
|
||||
<div style={{marginTop:6}}>
|
||||
<div style={{ marginTop: 6 }}>
|
||||
<MarkdownPreview content={post.attachedMarkdown.content} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginTop:8}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<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>
|
||||
<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 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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PostCard)
|
||||
export default memo(PostCard);
|
||||
|
||||
@@ -1,40 +1,48 @@
|
||||
import React from 'react'
|
||||
import { User } from '../types/User'
|
||||
import { Link } from 'react-router-dom'
|
||||
import useAppStore from '../store/useAppStore'
|
||||
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 currentUserId = useAppStore((s) => s.currentUserId);
|
||||
const endorseUser = useAppStore((s) => s.endorseUser);
|
||||
const addNotification = useAppStore((s) => s.addNotification);
|
||||
|
||||
const onEmailClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.preventDefault();
|
||||
if (!currentUserId) {
|
||||
addNotification('Select a current user (top-right) to endorse from.', 'error')
|
||||
return
|
||||
addNotification('Select a current user (top-right) to endorse from.', 'error');
|
||||
return;
|
||||
}
|
||||
if (currentUserId === user.id) {
|
||||
addNotification("You can't endorse yourself.", 'error')
|
||||
return
|
||||
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')
|
||||
}
|
||||
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>
|
||||
<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 className="tags-row" 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>
|
||||
<span key={s} className="tag">
|
||||
{s} ({user.endorsements[s] ?? 0})
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default UserCard
|
||||
export default UserCard;
|
||||
|
||||
Reference in New Issue
Block a user