/ opencodehub / src / components / admin / LiveLogStream.tsx
JustAPI Bot fix: comprehensive UI, docs, security, and deployment improvements · 7/14/2026
381467d
144 lines · 6.4 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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import { motion, AnimatePresence } from "framer-motion";
import { Radio, Clock, GitCommit, GitPullRequest, GitFork, Star, MessageSquare } from "lucide-react";
import { useEffect, useState } from "react";

interface LogEntry {
    id: string;
    timestamp: string;
    type: string;
    message: string;
    user: string;
    repo: string;
}

interface Props {
    logs: LogEntry[];
}

const getTypeIcon = (type: string) => {
    const iconClass = "w-3 h-3";
    switch (type.toLowerCase()) {
        case "push": return <GitCommit className={`${iconClass} text-green-400`} />;
        case "pr_open": return <GitPullRequest className={`${iconClass} text-blue-400`} />;
        case "fork": return <GitFork className={`${iconClass} text-purple-400`} />;
        case "star": return <Star className={`${iconClass} text-yellow-400`} />;
        case "comment": return <MessageSquare className={`${iconClass} text-cyan-400`} />;
        default: return <Radio className={`${iconClass} text-muted-foreground`} />;
    }
};

const getTypeColor = (type: string) => {
    switch (type.toLowerCase()) {
        case "push": return "text-green-400";
        case "pr_open": return "text-blue-400";
        case "fork": return "text-purple-400";
        case "star": return "text-yellow-400";
        case "comment": return "text-cyan-400";
        default: return "text-muted-foreground";
    }
};

const getTypeBg = (type: string) => {
    switch (type.toLowerCase()) {
        case "push": return "from-green-500/10 to-transparent";
        case "pr_open": return "from-blue-500/10 to-transparent";
        case "fork": return "from-purple-500/10 to-transparent";
        case "star": return "from-yellow-500/10 to-transparent";
        case "comment": return "from-cyan-500/10 to-transparent";
        default: return "from-gray-500/10 to-transparent";
    }
};

export default function LiveLogStream({ logs }: Props) {
    const [currentTime, setCurrentTime] = useState(new Date().toLocaleTimeString());

    useEffect(() => {
        const interval = setInterval(() => {
            setCurrentTime(new Date().toLocaleTimeString());
        }, 1000);
        return () => clearInterval(interval);
    }, []);

    return (
        <div className="p-6 h-full flex flex-col">
            {/* Header */}
            <div className="flex justify-between items-center mb-4 pb-3 border-b border-white/5">
                <div className="flex items-center gap-2">
                    <motion.div
                        animate={{ scale: [1, 1.2, 1] }}
                        transition={{ duration: 2, repeat: Infinity }}
                    >
                        <Radio className="w-4 h-4 text-red-400" />
                    </motion.div>
                    <div>
                        <h3 className="text-sm font-semibold bg-gradient-to-r from-gray-100 to-gray-400 bg-clip-text text-transparent">
                            Data Stream
                        </h3>
                        <div className="text-[10px] text-muted-foreground uppercase tracking-wider">Real-Time</div>
                    </div>
                </div>
                <div className="flex items-center gap-2 text-xs text-muted-foreground font-mono">
                    <Clock className="w-3 h-3" />
                    {currentTime}
                </div>
            </div>

            {/* Log entries */}
            <div className="flex-1 overflow-y-auto custom-scrollbar space-y-2">
                <AnimatePresence initial={false}>
                    {logs.map((log, index) => (
                        <motion.div
                            key={log.id}
                            initial={{ opacity: 0, height: 0, x: 20 }}
                            animate={{ opacity: 1, height: "auto", x: 0 }}
                            exit={{ opacity: 0, height: 0, x: -20 }}
                            transition={{ type: "spring", stiffness: 200, damping: 20 }}
                            className={`relative overflow-hidden rounded-lg bg-gradient-to-r ${getTypeBg(log.type)} p-2`}
                        >
                            {/* Timestamp */}
                            <div className="text-[10px] text-muted-foreground font-mono mb-1">
                                {log.timestamp}
                            </div>

                            {/* Content */}
                            <div className="flex items-start gap-2">
                                {/* Icon */}
                                <div className="mt-0.5">
                                    {getTypeIcon(log.type)}
                                </div>

                                {/* Details */}
                                <div className="flex-1 min-w-0 text-xs">
                                    <div className="flex flex-wrap gap-x-1.5 items-center">
                                        <span className="text-muted-foreground font-mono">#{log.id.substring(0, 7)}</span>
                                        <span className="text-muted-foreground">:</span>
                                        <span className={`font-semibold ${getTypeColor(log.type)}`}>
                                            {log.type.toUpperCase().replace("_", " ")}
                                        </span>
                                    </div>
                                    <div className="text-muted-foreground truncate mt-0.5">
                                        {log.message}
                                    </div>
                                </div>
                            </div>

                            {/* Animated line for new entries */}
                            {index === 0 && (
                                <motion.div
                                    className={`absolute left-0 top-0 bottom-0 w-0.5 bg-gradient-to-b ${getTypeColor(log.type).replace("text", "from")} to-transparent`}
                                    initial={{ opacity: 0 }}
                                    animate={{ opacity: [0, 1, 0] }}
                                    transition={{ duration: 2 }}
                                />
                            )}
                        </motion.div>
                    ))}
                </AnimatePresence>
            </div>

            {/* Fade effect at bottom */}
            <div className="absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-[#0a0a15] to-transparent pointer-events-none" />
        </div>
    );
}