swadhinbiswas Initial release v0.1.0 · 1/16/2026
ed7a4e4
118 lines · 4.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import { motion } from 'framer-motion';
import { MessageCircle, Image as ImageIcon, Pin, Lock } from 'lucide-react';
import { CatalogThread } from '../../types/api';
import { getThumbnailUrl } from '../../utils/apiClient';
import { extractPlainText } from '../../utils/parser';
import { formatNumber } from '../../utils/formatters';
import { getBoardTheme } from '../../lib/design-tokens';

interface ThreadCardProps {
    thread: CatalogThread;
    board: string;
    onClick: () => void;
}

export default function ThreadCard({ thread, board, onClick }: ThreadCardProps) {
    const themeColor = getBoardTheme(board);
    const excerpt = extractPlainText(thread.com, 150);
    const hasThumbnail = thread.tim && thread.ext;

    return (
        <motion.div
            whileHover={{
                scale: 1.02,
                y: -4,
                boxShadow: '0 10px 40px -10px rgba(0,0,0,0.5)'
            }}
            whileTap={{ scale: 0.98 }}
            transition={{ type: 'spring', stiffness: 400, damping: 25 }}
            onClick={onClick}
            className="thread-card cursor-pointer bg-dark-surface rounded-lg overflow-hidden border border-dark-border hover:border-primary-500/50 transition-colors"
            style={{
                borderTopColor: themeColor,
                borderTopWidth: '3px',
            }}
        >
            {/* Thumbnail */}
            {hasThumbnail ? (
                <div className="aspect-video bg-dark-elevated relative overflow-hidden">
                    <img
                        src={getThumbnailUrl(board, thread.tim!)}
                        alt={thread.filename || 'Thread image'}
                        className="w-full h-full object-cover"
                        loading="lazy"
                    />
                    {/* Badges */}
                    <div className="absolute top-2 right-2 flex gap-1">
                        {thread.sticky === 1 && (
                            <div className="bg-green-600 p-1 rounded">
                                <Pin className="w-3 h-3 text-white" />
                            </div>
                        )}
                        {thread.closed === 1 && (
                            <div className="bg-red-600 p-1 rounded">
                                <Lock className="w-3 h-3 text-white" />
                            </div>
                        )}
                    </div>
                </div>
            ) : (
                <div className="aspect-video bg-dark-elevated flex items-center justify-center">
                    <MessageCircle className="w-12 h-12 text-gray-600" />
                </div>
            )}

            {/* Content */}
            <div className="p-3">
                {/* Subject */}
                {thread.sub && (
                    <h3 className="font-semibold text-sm mb-1 truncate" title={thread.sub}>
                        {thread.sub}
                    </h3>
                )}

                {/* Excerpt */}
                {excerpt && (
                    <p className="text-xs text-gray-400 truncate-2 mb-2 leading-relaxed">
                        {excerpt}
                    </p>
                )}

                {/* Stats */}
                <div className="flex items-center gap-3 text-xs text-gray-400">
                    <div className="flex items-center gap-1">
                        <MessageCircle className="w-3 h-3" />
                        <span>{formatNumber(thread.replies)}</span>
                    </div>
                    <div className="flex items-center gap-1">
                        <ImageIcon className="w-3 h-3" />
                        <span>{formatNumber(thread.images)}</span>
                    </div>
                    {thread.unique_ips && (
                        <div className="flex items-center gap-1">
                            <span className="text-[10px]">👤</span>
                            <span>{thread.unique_ips}</span>
                        </div>
                    )}
                </div>

                {/* Limit indicators */}
                {(thread.bumplimit === 1 || thread.imagelimit === 1) && (
                    <div className="flex gap-1 mt-2">
                        {thread.bumplimit === 1 && (
                            <span className="text-[10px] px-2 py-0.5 bg-orange-600/20 text-orange-400 rounded">
                                Bump Limit
                            </span>
                        )}
                        {thread.imagelimit === 1 && (
                            <span className="text-[10px] px-2 py-0.5 bg-blue-600/20 text-blue-400 rounded">
                                Image Limit
                            </span>
                        )}
                    </div>
                )}
            </div>
        </motion.div>
    );
}