diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-05-06 00:05:25 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-05-06 00:10:40 -0700 |
| commit | 913f28e2f27830192a1c80270612d8314eed3353 (patch) | |
| tree | 01bdf287a3b7c2370b37b3b1eba0be4ba5868479 /src | |
| parent | 83c3fa302adbc4c3adf63c59cfe87b51d83ecbcb (diff) | |
phase_tracker_only: implement the twitch table
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/SubscriberTable/SubscriberTable.tsx | 12 | ||||
| -rw-r--r-- | src/components/SubscriberTable/TwitchDataTable.tsx | 182 | ||||
| -rw-r--r-- | src/components/SubscriberTable/TwitchTableRow.tsx | 70 | ||||
| -rw-r--r-- | src/components/TitleBar/TitleBar.tsx | 5 | ||||
| -rw-r--r-- | src/pages/twitch/index.tsx | 65 |
5 files changed, 332 insertions, 2 deletions
diff --git a/src/components/SubscriberTable/SubscriberTable.tsx b/src/components/SubscriberTable/SubscriberTable.tsx index c308aee..6a73f71 100644 --- a/src/components/SubscriberTable/SubscriberTable.tsx +++ b/src/components/SubscriberTable/SubscriberTable.tsx @@ -1,5 +1,6 @@ "use client"; import React, { useState } from "react"; +import Link from "next/link"; import ChannelRow from "./SubscriberTableRow"; interface ChannelDataProp { @@ -20,7 +21,7 @@ interface SubscriberDataTableProp { timestamp: string; } -type SortKey = keyof ChannelDataProp | "rank"; +type SortKey = keyof ChannelDataProp | 'rank'; const DataTable = ({ channel_data, timestamp }: SubscriberDataTableProp) => { const [sortKey, setSortKey] = useState<SortKey>("subscribers"); @@ -73,6 +74,13 @@ const DataTable = ({ channel_data, timestamp }: SubscriberDataTableProp) => { <p className="text-gray-500 text-sm"> Updated Hourly. Retrieved at: {timestamp} </p> + <Link href="/twitch"> + <button + className="mt-4 px-4 py-2 bg-black text-white font-semibold rounded-md hover:bg-gray-800 transition-colors" + > + Looking for "The Twitch Table"? + </button> + </Link> </div> <div className="px-2 sm:px-48 py-4 sm:py-8 relative rounded-l text-left overflow-auto"> <table className="w-full text-m sm:text-xl text-black bg-white"> @@ -159,4 +167,4 @@ const DataTable = ({ channel_data, timestamp }: SubscriberDataTableProp) => { export default DataTable; export type { SubscriberDataTableProp }; -export type { ChannelDataProp };
\ No newline at end of file +export type { ChannelDataProp }; diff --git a/src/components/SubscriberTable/TwitchDataTable.tsx b/src/components/SubscriberTable/TwitchDataTable.tsx new file mode 100644 index 0000000..3c0680c --- /dev/null +++ b/src/components/SubscriberTable/TwitchDataTable.tsx @@ -0,0 +1,182 @@ +"use client"; +import React, { useState } from "react"; +import ChannelRow from "./TwitchTableRow"; + +interface TwitchChannelDataProp { + channel_name: string; + profile_pic: string; + subscribers: number; + sub_org: string; + twitch_followers: number; + total_sum: number; + max_following?: number; +} + + +interface TwitchDataTableProp { + channel_data: TwitchChannelDataProp[]; + timestamp: string; +} + +type SortKey = keyof TwitchChannelDataProp | "rank" | "max_following"; + +const TwitchDataTable = ({ channel_data, timestamp }: TwitchDataTableProp) => { + const [sortKey, setSortKey] = useState<SortKey>("twitch_followers"); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); + const [indexName, setIndexName] = useState<string>("RANK"); + + const handleSort = (key: SortKey) => { + if (sortKey === key) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + } else { + setSortKey(key); + setSortOrder("desc"); + } + setIndexName(key === "sub_org" ? "INDEX" : "RANK"); + }; + + const dataWithMax = channel_data.map((channel) => ({ + ...channel, + max_following: Math.max( + channel.subscribers || 0, + channel.twitch_followers || 0 + ), + })); + + const sortedData = [...dataWithMax].sort((a, b) => { + let aValue: any, bValue: any; + if (sortKey === "rank") { + aValue = dataWithMax.indexOf(a) + 1; + bValue = dataWithMax.indexOf(b) + 1; + } else { + aValue = a[sortKey]; + bValue = b[sortKey]; + } + if (typeof aValue === "string") { + return sortOrder === "asc" + ? aValue.localeCompare(bValue) + : bValue.localeCompare(aValue); + } + return sortOrder === "asc" ? aValue - bValue : bValue - aValue; + }); + + return ( + <> + <div className="sm:hidden text-center text-red-600 font-semibold my-2"> + Limited data shown on mobile view! + </div> + <div className="text-center sm:mt-5" style={{ fontFamily: "Quantico, sans-serif" }}> + <h1 className="text-2xl font-bold text-gray-800">The Twitch Table</h1> + <p className="text-gray-500 text-sm">Updated Hourly. Retrieved at: {timestamp}</p> + </div> + + <div className="flex justify-center mt-2 mb-4"> + <div className="bg-gray-100 rounded-lg p-4 shadow-sm text-sm md:text-base max-w-2xl"> + {/* Legend wrapper - stacked sections on all screen sizes */} + <div className="flex flex-col gap-3"> + + {/* Column explanations */} + <div> + <h3 className="font-bold text-gray-800 mb-1">Column Explanations</h3> + <div> + <div className="flex items-start mb-1"> + <span className="font-medium text-gray-700 mr-2 whitespace-nowrap">SUM(YT+TTV):</span> + <span className="text-gray-600">Total combined followers across both platforms</span> + </div> + <div className="flex items-start"> + <span className="font-medium text-gray-700 mr-2 whitespace-nowrap">MAX(YT,TTV):</span> + <span className="text-gray-600">Highest follower count between platforms</span> + </div> + </div> + </div> + + {/* Horizontal divider */} + <div className="border-t border-gray-300 w-full"></div> + + {/* Color key */} + <div> + <h3 className="font-bold text-gray-800 mb-1">MAX Column Color Key</h3> + <div> + <div className="flex items-center mb-1"> + <div className="h-3 w-3 bg-red-600 rounded-full mr-2 flex-shrink-0"></div> + <span className="text-gray-600">YouTube subscriber count is higher</span> + </div> + <div className="flex items-center"> + <div className="h-3 w-3 bg-purple-600 rounded-full mr-2 flex-shrink-0"></div> + <span className="text-gray-600">Twitch follower count is higher</span> + </div> + </div> + </div> + </div> + </div> + </div> + + <div className="px-2 sm:px-48 py-4 sm:py-8 relative rounded-l text-left overflow-auto"> + <table className="w-full text-m sm:text-xl text-black bg-white"> + <thead + className="text-m sm:text-lg text-white rounded-md select-none" + style={{ backgroundColor: "black" }} + > + <tr> + <th className="py-1 px-1 sm:px-3 hidden sm:table-cell">{indexName}</th> + <th className="py-1 px-1 sm:px-3">CHANNEL</th> + <th + className="py-1 px-1 sm:px-3 hidden sm:table-cell cursor-pointer" + onClick={() => handleSort("sub_org")} + > + GROUP + {sortKey === "sub_org" && ( + <span className="ml-1">{sortOrder === "asc" ? "▲" : "▼"}</span> + )} + </th> + <th + className="py-1 px-1 sm:px-3 cursor-pointer" + onClick={() => handleSort("subscribers")} + > + YOUTUBE SUBS + {sortKey === "subscribers" && ( + <span className="ml-1">{sortOrder === "asc" ? "▲" : "▼"}</span> + )} + </th> + <th + className="py-1 px-1 sm:px-3 cursor-pointer" + onClick={() => handleSort("twitch_followers")} + > + TWITCH FOLLOWS + {sortKey === "twitch_followers" && ( + <span className="ml-1">{sortOrder === "asc" ? "▲" : "▼"}</span> + )} + </th> + <th + className="py-1 px-1 sm:px-3 cursor-pointer" + onClick={() => handleSort("total_sum")} + > + SUM(YT+TTV) + {sortKey === "total_sum" && ( + <span className="ml-1">{sortOrder === "asc" ? "▲" : "▼"}</span> + )} + </th> + <th + className="py-1 px-1 sm:px-3 cursor-pointer" + onClick={() => handleSort("max_following")} + > + MAX(YT, TTV) + {sortKey === "max_following" && ( + <span className="ml-1">{sortOrder === "asc" ? "▲" : "▼"}</span> + )} + </th> + </tr> + </thead> + <tbody> + {sortedData.map((channel, index) => ( + <ChannelRow key={index} channel={channel} index={index} /> + ))} + </tbody> + </table> + </div> + </> + ); +}; + +export default TwitchDataTable; +export type { TwitchDataTableProp, TwitchChannelDataProp }; diff --git a/src/components/SubscriberTable/TwitchTableRow.tsx b/src/components/SubscriberTable/TwitchTableRow.tsx new file mode 100644 index 0000000..b4aae9c --- /dev/null +++ b/src/components/SubscriberTable/TwitchTableRow.tsx @@ -0,0 +1,70 @@ +"use client"; +import Image from "next/image"; +import type React from "react"; +import type { TwitchChannelDataProp } from "./TwitchDataTable"; + +interface TwitchRowProps { + channel: TwitchChannelDataProp; + index: number; +} + +const TwitchTableRow: React.FC<TwitchRowProps> = ({ channel, index }) => ( + <tr + key={index} + className="border-b hover:bg-gray-100 cursor-pointer" + onClick={() => (window.location.href = "/stats/" + channel.channel_name)} + > + <td className="py-3 px-1 sm:px-3 hidden sm:table-cell">{index + 1}</td> + <td className="py-3 px-1 sm:px-3 flex items-center"> + <Image + src={channel.profile_pic} + alt={channel.channel_name} + width={50} + height={50} + className="rounded-full" + /> + <span className="ml-2 hidden sm:block">{channel.channel_name}</span> + <span className="ml-2 sm:hidden">{ + (() => { + const words = channel.channel_name.split(' '); + if (words.length >= 2) { + return `${words[0][0]}.${words[1][0]}`; + } else if (words.length === 1) { + return `${words[0][0]}.`; + } + return ''; + })() + }</span> + </td> + <td className="py-3 px-1 sm:px-3 hidden sm:table-cell"> + {channel.sub_org} + </td> + <td className="py-3 px-1 sm:px-3"> + {Number(channel.subscribers).toLocaleString()} + </td> + <td className="py-3 px-1 sm:px-3"> + {Number(channel.twitch_followers).toLocaleString()} + </td> + <td className="py-3 px-1 sm:px-3 hidden sm:table-cell"> + {Number(channel.total_sum).toLocaleString()} + </td> + <td className="py-2 px-3 font-bold"> + {channel.max_following !== undefined && ( + <span + className={ + channel.twitch_followers > channel.subscribers + ? "text-purple-500" // Twitch color + : channel.subscribers > channel.twitch_followers + ? "text-red-600" // YouTube color + : "" + } + > + {channel.max_following.toLocaleString() || "0"} + </span> + )} +</td> + + </tr> +); + +export default TwitchTableRow; diff --git a/src/components/TitleBar/TitleBar.tsx b/src/components/TitleBar/TitleBar.tsx index af34446..f345f24 100644 --- a/src/components/TitleBar/TitleBar.tsx +++ b/src/components/TitleBar/TitleBar.tsx @@ -134,6 +134,11 @@ const TitleBar: React.FC<TitleBarProps> = ({ Home </li> </Link> + <Link href="/twitch"> + <li className="p-4 hover:bg-gray-700 transition-colors duration-300"> + Twitch Table + </li> + </Link> <Link href="/about"> <li className="p-4 hover:bg-gray-700 transition-colors duration-300"> About diff --git a/src/pages/twitch/index.tsx b/src/pages/twitch/index.tsx new file mode 100644 index 0000000..43d88ed --- /dev/null +++ b/src/pages/twitch/index.tsx @@ -0,0 +1,65 @@ +import { useEffect, useState } from "react"; +import TwitchDataTable, { + type TwitchDataTableProp, +} from "../../components/SubscriberTable/TwitchDataTable"; +import TitleBar from "../../components/TitleBar/TitleBar"; +import Announcement from "../../components/Announcement"; +import "../../app/globals.css"; + +function TwitchPage() { + const [twitchData, setTwitchData] = useState<TwitchDataTableProp | null>(null); + const [error, setError] = useState<string | null>(null); + + const announcementText = process.env.NEXT_PUBLIC_ANNOUNCEMENT; + + useEffect(() => { + async function fetchTwitchData() { + try { + const apiUrl = process.env.NEXT_PUBLIC_API_URL_TESTING; + const endpoint = "/api/twitch"; + const headers = { + "Cache-Control": "no-cache", + }; + const cacheOption = "no-cache"; + + const response = await fetch(`${apiUrl}${endpoint}`, { + headers: headers, + cache: cacheOption, + }); + + if (!response.ok) { + throw new Error(response.statusText); + } + + const data = await response.json(); + setTwitchData(data); + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred"); + } + } + + fetchTwitchData(); + }, []); + + return ( + <> + <TitleBar title="PhaseTracker" backgroundColor="black" /> + {announcementText && ( + <Announcement + message={announcementText} + backgroundColor="#e0f7fa" + textColor="#006064" + /> + )} + {error ? ( + <div>Error: {error}</div> + ) : twitchData ? ( + <TwitchDataTable {...twitchData} /> + ) : ( + <div>Loading...</div> + )} + </> + ); +} + +export default TwitchPage; |
