chore : move all to root
This commit is contained in:
188
nest-front/src/pages/public/ForumPage.tsx
Normal file
188
nest-front/src/pages/public/ForumPage.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { MOCK_CATEGORIES, MOCK_THREADS } from '../../data/mockData';
|
||||
import { timeAgo } from '../../utils/format';
|
||||
import type { ForumCategory, ForumThread } from '../../types';
|
||||
|
||||
// ── Sub-components ─────────────────────────────────────────────────────────────
|
||||
|
||||
function CategoryCard({ category, threads }: { category: ForumCategory; threads: ForumThread[] }) {
|
||||
const pinned = threads.filter((t) => t.isPinned && t.categoryId === category.id);
|
||||
const regular = threads.filter((t) => !t.isPinned && t.categoryId === category.id);
|
||||
const categoryThreads = [...pinned, ...regular];
|
||||
|
||||
return (
|
||||
<section className="crt-box" style={{ marginBottom: '1.5rem' }}>
|
||||
{/* Category header */}
|
||||
<div
|
||||
style={{
|
||||
padding: '1rem 1.5rem',
|
||||
borderBottom: '1px solid var(--color-border)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '1rem',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
color: 'var(--color-green)',
|
||||
fontSize: '0.75rem',
|
||||
opacity: 0.7,
|
||||
}}
|
||||
>
|
||||
{category.icon}
|
||||
</span>
|
||||
<div>
|
||||
<h2
|
||||
style={{
|
||||
fontFamily: 'var(--font-heading)',
|
||||
color: 'var(--color-text)',
|
||||
fontSize: '1.1rem',
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{category.name}
|
||||
</h2>
|
||||
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.75rem', margin: 0 }}>
|
||||
{category.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
color: 'var(--color-text-muted)',
|
||||
fontSize: '0.72rem',
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--color-green)' }}>{category.threadCount}</span> threads
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Threads */}
|
||||
<div>
|
||||
{categoryThreads.length === 0 ? (
|
||||
<div style={{ padding: '1.5rem', color: 'var(--color-text-muted)', fontSize: '0.8rem', textAlign: 'center' }}>
|
||||
No threads yet. Be the first to post.
|
||||
</div>
|
||||
) : (
|
||||
categoryThreads.map((thread, idx) => (
|
||||
<div
|
||||
key={thread.id}
|
||||
style={{
|
||||
padding: '0.85rem 1.5rem',
|
||||
borderBottom: idx < categoryThreads.length - 1 ? '1px solid var(--color-border)' : 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '1rem',
|
||||
flexWrap: 'wrap',
|
||||
background: thread.isPinned ? 'rgba(217,119,6,0.05)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: '0' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap', marginBottom: '0.25rem' }}>
|
||||
{thread.isPinned && (
|
||||
<span className="badge badge-progress">Pinned</span>
|
||||
)}
|
||||
{thread.isLocked && (
|
||||
<span className="badge badge-closed">Locked</span>
|
||||
)}
|
||||
<Link
|
||||
to={`/forum/thread/${thread.id}`}
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
color: 'var(--color-text)',
|
||||
fontSize: '0.87rem',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{thread.title}
|
||||
</Link>
|
||||
</div>
|
||||
<div style={{ color: 'var(--color-text-muted)', fontSize: '0.72rem', fontFamily: 'var(--font-mono)' }}>
|
||||
by <span style={{ color: 'var(--color-text-dim)' }}>{thread.authorName}</span>
|
||||
{' '}— {timeAgo(thread.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right', fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--color-text-muted)', flexShrink: 0 }}>
|
||||
<div style={{ color: 'var(--color-green)' }}>{thread.replyCount}</div>
|
||||
<div>replies</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Forum Page ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function ForumPage() {
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const filteredCategories = useMemo(() => {
|
||||
if (!search.trim()) return MOCK_CATEGORIES;
|
||||
const q = search.toLowerCase();
|
||||
return MOCK_CATEGORIES.filter((cat) =>
|
||||
cat.name.toLowerCase().includes(q) ||
|
||||
MOCK_THREADS.some((t) => t.categoryId === cat.id && t.title.toLowerCase().includes(q))
|
||||
);
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '960px', margin: '0 auto', padding: '4rem 1.5rem' }}>
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: '2.5rem', display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', flexWrap: 'wrap', gap: '1.5rem' }}>
|
||||
<div>
|
||||
<div className="section-label">Community</div>
|
||||
<h1
|
||||
style={{
|
||||
fontFamily: 'var(--font-heading)',
|
||||
color: 'var(--color-text)',
|
||||
fontSize: 'clamp(2rem, 5vw, 3rem)',
|
||||
marginTop: '0.5rem',
|
||||
}}
|
||||
>
|
||||
FORUM
|
||||
</h1>
|
||||
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.8rem', marginTop: '0.5rem', fontFamily: 'var(--font-mono)' }}>
|
||||
Read freely. Login to post.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||
<input
|
||||
className="input-terminal"
|
||||
type="search"
|
||||
placeholder="Search threads..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
style={{ width: '220px' }}
|
||||
aria-label="Search forum threads"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
{filteredCategories.length === 0 ? (
|
||||
<div className="crt-box" style={{ padding: '2rem', textAlign: 'center', color: 'var(--color-text-muted)', fontFamily: 'var(--font-mono)' }}>
|
||||
No results found for "{search}"
|
||||
</div>
|
||||
) : (
|
||||
filteredCategories.map((cat) => (
|
||||
<CategoryCard key={cat.id} category={cat} threads={MOCK_THREADS} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user