44 < meta charset ="UTF-8 " />
55 < title > 7TV Live Emote Tracker</ title >
66 < style >
7- * {
8- box-sizing : border-box;
9- }
10- body {
11- margin : 0 ;
12- font-family : 'Inter' , sans-serif;
13- background : # 0a0013 ;
14- color : # fff ;
15- text-align : center;
16- min-height : 100vh ;
17- background-image : url ("peepo.png" );
18- background-size : cover;
19- background-position : center;
20- background-attachment : fixed;
21- background-repeat : no-repeat;
22- overflow-x : hidden;
23- }
24-
25- body ::before {
26- content : "" ;
27- position : fixed;
28- inset : 0 ;
29- background : radial-gradient (circle at top left, rgba (90 , 0 , 255 , 0.4 ), transparent),
30- radial-gradient (circle at bottom right, rgba (180 , 0 , 255 , 0.25 ), transparent),
31- rgba (0 , 0 , 0 , 0.85 );
32- backdrop-filter : blur (12px );
33- z-index : -1 ;
34- }
35-
36- h1 {
37- margin-top : 1.2rem ;
38- font-size : 2rem ;
39- color : # c8a9ff ;
40- text-shadow : 0 0 10px # a76eff, 0 0 25px # 6b2cff ;
41- animation : glowPulse 2s infinite alternate;
42- }
43-
44- @keyframes glowPulse {
45- from { text-shadow : 0 0 8px # 9b59ff, 0 0 16px # 823bff ; }
46- to { text-shadow : 0 0 18px # bb8cff, 0 0 32px # b57bff ; }
47- }
48-
49- input , button {
50- padding : 0.6rem 1rem ;
51- border : 2px solid # 9146FF ;
52- border-radius : 10px ;
53- background : # 161024 ;
54- color : white;
55- margin : 0.5rem ;
56- font-size : 1rem ;
57- transition : 0.3s ;
58- box-shadow : 0 0 10px rgba (145 , 70 , 255 , 0.3 );
59- }
60-
61- input : focus {
62- border-color : # b68cff ;
63- outline : none;
64- box-shadow : 0 0 12px # b68cff ;
65- }
66-
67- button {
68- background : linear-gradient (90deg , # 9146FF, # b88cff );
69- cursor : pointer;
70- font-weight : bold;
71- box-shadow : 0 0 12px # 9146ff7a ;
72- }
73-
74- button : hover {
75- box-shadow : 0 0 18px # b88cff ;
76- transform : scale (1.03 );
77- }
78-
79- # title {
80- margin-top : 0.8rem ;
81- font-size : 1.2rem ;
82- color : # c7b6ff ;
83- text-shadow : 0 0 8px # 8f6cff ;
84- }
85-
86- # tables {
87- display : flex;
88- justify-content : space-around;
89- flex-wrap : wrap;
90- margin-top : 2rem ;
91- }
92-
93- .table-container {
94- width : 45% ;
95- min-width : 320px ;
96- background : rgba (10 , 0 , 25 , 0.6 );
97- border : 2px solid rgba (130 , 60 , 255 , 0.4 );
98- border-radius : 15px ;
99- box-shadow : 0 0 20px rgba (145 , 70 , 255 , 0.15 );
100- padding : 1rem ;
101- transition : 0.3s ;
102- }
103-
104- .table-container : hover {
105- box-shadow : 0 0 30px rgba (160 , 90 , 255 , 0.3 );
106- }
107-
108- h2 {
109- color : # d2b6ff ;
110- text-shadow : 0 0 8px # a66eff ;
111- font-weight : 600 ;
112- }
113-
114- table {
115- width : 100% ;
116- border-collapse : collapse;
117- margin-top : 1rem ;
118- background : rgba (25 , 10 , 40 , 0.6 );
119- border-radius : 10px ;
120- overflow : hidden;
121- }
122-
123- th , td {
124- padding : 10px 16px ;
125- border-bottom : 1px solid rgba (90 , 50 , 160 , 0.3 );
126- }
127-
128- th {
129- background : rgba (90 , 40 , 160 , 0.3 );
130- color : # c6a3ff ;
131- text-transform : uppercase;
132- font-size : 0.85rem ;
133- letter-spacing : 1px ;
134- }
135-
136- td {
137- color : # fff ;
138- font-size : 1rem ;
139- }
140-
141- tr : nth-child (even) {
142- background : rgba (50 , 30 , 90 , 0.4 );
143- }
144-
145- tr : hover {
146- background : rgba (100 , 50 , 200 , 0.4 );
147- transition : 0.2s ;
148- }
7+ /* (Keep your CSS here, unchanged for brevity) */
1498 </ style >
1509</ head >
15110< body >
15211 < h1 > 💫 7TV Live Tracker</ h1 >
15312
15413 < div >
15514 < input id ="channelInput " placeholder ="Enter Twitch channel... " />
156- < button onclick ="loadChannel () "> Track Channel</ button >
15+ < button onclick ="connectStreamElements () "> Track Channel</ button >
15716 </ div >
15817 < div id ="title "> </ div >
15918
@@ -176,45 +35,99 @@ <h2>🎭 Emote Leaderboard</h2>
17635 </ div >
17736
17837 < script >
179- const ws = new WebSocket ( `ws://${ location . host } ` ) ;
180- let channel = null ;
181- let users = { } ;
38+ let seSocket = null ;
39+ let userUsage = { } ;
18240 let emoteUsage = { } ;
41+ let channel = null ;
18342
184- ws . onmessage = ( e ) => {
185- const data = JSON . parse ( e . data ) ;
186- if ( data . channel === channel && data . type === "update" ) {
187- users [ data . user . id ] = data . user ;
188- emoteUsage = data . emoteUsage ;
189- render ( ) ;
190- }
191- } ;
192-
193- async function loadChannel ( ) {
43+ function connectStreamElements ( ) {
19444 channel = document . getElementById ( "channelInput" ) . value . trim ( ) . toLowerCase ( ) ;
195- if ( ! channel ) return ;
196- const res = await fetch ( `/api/channel/${ channel } ` ) ;
197- const data = await res . json ( ) ;
198- users = { } ;
199- data . users . forEach ( ( u ) => ( users [ u . id ] = u ) ) ;
200- emoteUsage = data . emoteUsage ;
45+ if ( ! channel ) return alert ( "Enter a Twitch channel name" ) ;
46+
20147 document . getElementById ( "title" ) . innerText = `Tracking: ${ channel } ` ;
202- render ( ) ;
48+ userUsage = { } ;
49+ emoteUsage = { } ;
50+
51+ if ( seSocket ) seSocket . close ( ) ;
52+
53+ seSocket = new WebSocket ( "wss://realtime.streamelements.com/socket" ) ;
54+
55+ seSocket . onopen = ( ) => {
56+ console . log ( "✅ Connected to StreamElements API" ) ;
57+ } ;
58+
59+ seSocket . onmessage = ( event ) => {
60+ const data = JSON . parse ( event . data ) ;
61+
62+ if ( data . type === "welcome" ) {
63+ console . log ( "👋 Welcome:" , data . payload . sessionId ) ;
64+
65+ // Authenticate using public channel (no token needed for chat)
66+ seSocket . send (
67+ JSON . stringify ( {
68+ type : "authenticate" ,
69+ payload : {
70+ method : "jwt" ,
71+ token : null
72+ }
73+ } )
74+ ) ;
75+
76+ // Subscribe to chat for the specified channel
77+ // You’ll need the channel’s StreamElements channel ID
78+ fetch ( `https://api.streamelements.com/kappa/v2/channels/${ channel } ` )
79+ . then ( ( r ) => r . json ( ) )
80+ . then ( ( ch ) => {
81+ console . log ( "🎯 Subscribing to channel:" , ch . _id , ch . username ) ;
82+ seSocket . send (
83+ JSON . stringify ( {
84+ type : "listen" ,
85+ event : `message:${ ch . _id } ` ,
86+ } )
87+ ) ;
88+ } )
89+ . catch ( ( err ) => {
90+ console . error ( "❌ Failed to get channel:" , err ) ;
91+ } ) ;
92+ }
93+
94+ if ( data . type === "ping" ) {
95+ seSocket . send ( JSON . stringify ( { type : "pong" } ) ) ;
96+ }
97+
98+ if ( data . type === "event" && data . event ?. type === "message" ) {
99+ const msg = data . event . data ;
100+ const username = msg . nick || "Unknown" ;
101+ const emotes = msg . emotes || [ ] ;
102+
103+ userUsage [ username ] = ( userUsage [ username ] || 0 ) + 1 ;
104+
105+ emotes . forEach ( ( e ) => {
106+ const name = e . text || e . name || "unknown" ;
107+ emoteUsage [ name ] = ( emoteUsage [ name ] || 0 ) + 1 ;
108+ } ) ;
109+
110+ render ( ) ;
111+ }
112+ } ;
113+
114+ seSocket . onerror = ( err ) => console . error ( "WebSocket error:" , err ) ;
115+ seSocket . onclose = ( ) => console . log ( "⚠️ Disconnected from StreamElements" ) ;
203116 }
204117
205118 function render ( ) {
206- const sortedUsers = Object . values ( users ) . sort ( ( a , b ) => b . count - a . count ) ;
207- const sortedEmotes = Object . entries ( emoteUsage )
119+ const sortedUsers = Object . entries ( userUsage )
208120 . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
209- . map ( ( [ name , count ] ) => ( { name, count } ) ) ;
210-
211- document . getElementById ( "userboard" ) . innerHTML = sortedUsers
212- . map ( ( u , i ) => `<tr><td>${ i + 1 } </td><td>${ u . username } </td><td>${ u . count } </td></tr>` )
121+ . map ( ( [ username , count ] , i ) => `<tr><td>${ i + 1 } </td><td>${ username } </td><td>${ count } </td></tr>` )
213122 . join ( "" ) ;
214123
215- document . getElementById ( "emoteboard" ) . innerHTML = sortedEmotes
216- . map ( ( e , i ) => `<tr><td>${ i + 1 } </td><td>${ e . name } </td><td>${ e . count } </td></tr>` )
124+ const sortedEmotes = Object . entries ( emoteUsage )
125+ . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
126+ . map ( ( [ emote , count ] , i ) => `<tr><td>${ i + 1 } </td><td>${ emote } </td><td>${ count } </td></tr>` )
217127 . join ( "" ) ;
128+
129+ document . getElementById ( "userboard" ) . innerHTML = sortedUsers ;
130+ document . getElementById ( "emoteboard" ) . innerHTML = sortedEmotes ;
218131 }
219132 </ script >
220133</ body >
0 commit comments