diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/layout.tsx | 44 | ||||
| -rw-r--r-- | src/app/page.tsx | 60 | ||||
| -rw-r--r-- | src/components/CompactTable/CompactTable.tsx | 65 | ||||
| -rw-r--r-- | src/components/DataChart/DataChart.tsx | 130 | ||||
| -rw-r--r-- | src/components/Divider/Divider.tsx | 18 | ||||
| -rw-r--r-- | src/components/Footer/Footer.tsx | 48 | ||||
| -rw-r--r-- | src/components/SubscriberTable/SubscriberTable.tsx | 123 | ||||
| -rw-r--r-- | src/components/SubscriberTable/SubscriberTableRow.tsx | 70 | ||||
| -rw-r--r-- | src/components/TitleBar/TitleBar.tsx | 71 | ||||
| -rw-r--r-- | src/components/channel-card.tsx | 131 | ||||
| -rw-r--r-- | src/components/ui/avatar.tsx | 76 | ||||
| -rw-r--r-- | src/components/ui/badge.tsx | 54 | ||||
| -rw-r--r-- | src/components/ui/card.tsx | 125 | ||||
| -rw-r--r-- | src/lib/utils.ts | 6 | ||||
| -rw-r--r-- | src/pages/stats/[slug].tsx | 293 |
15 files changed, 747 insertions, 567 deletions
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 65445e8..ef1eb9d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,28 +1,30 @@ -import Head from 'next/head' -import type { Metadata } from 'next' -import { Inter } from 'next/font/google' -import Footer from '../components/Footer/Footer' -import { Analytics } from "@vercel/analytics/react" -import './globals.css' +import { Analytics } from "@vercel/analytics/react"; +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import Head from "next/head"; +import Footer from "../components/Footer/Footer"; +import "./globals.css"; -const inter = Inter({ subsets: ['latin'] }) +const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: 'PhaseTracker - Phase Connect Subscriber Tracker', - description: 'PhaseTracker, historical subscriber data for members of Phase Connect', -} + title: "PhaseTracker - Phase Connect Subscriber Tracker", + description: + "PhaseTracker, historical subscriber data for members of Phase Connect", +}; export default function RootLayout({ - children, + children, }: { - children: React.ReactNode + children: React.ReactNode; }) { - return ( - <html lang="en"> - <body className={inter.className}>{children} - <Footer /> - <Analytics/> - </body> - </html> - ) -}
\ No newline at end of file + return ( + <html lang="en"> + <body className={inter.className}> + {children} + <Footer /> + <Analytics /> + </body> + </html> + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 4749635..b5db5b6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,31 +1,41 @@ -import SubscriberTable, { SubscriberDataTableProp } from '../components/SubscriberTable/SubscriberTable'; -import TitleBar from '../components/TitleBar/TitleBar'; +import SubscriberTable, { + type SubscriberDataTableProp, +} from "../components/SubscriberTable/SubscriberTable"; +import TitleBar from "../components/TitleBar/TitleBar"; async function Home() { - const graphURL = process.env.NEXT_PUBLIC_GRAPH_URL - const data: SubscriberDataTableProp = await getData(); - return ( - <> - <TitleBar title="PhaseTracker" backgroundColor='black' /> - <div className="sm:block hidden mt-4" style={{ overflow: 'hidden', height: '105vh', position: 'relative' }}> - <iframe src={graphURL} style={{ position: 'absolute', top: 0, left: 0 }} width="100%" height="100%"></iframe> - </div> - <SubscriberTable {...data} /> - </> - ); + const graphURL = process.env.NEXT_PUBLIC_GRAPH_URL; + const data: SubscriberDataTableProp = await getData(); + return ( + <> + <TitleBar title="PhaseTracker" backgroundColor="black" /> + <div + className="sm:block hidden mt-4" + style={{ overflow: "hidden", height: "105vh", position: "relative" }} + > + <iframe + src={graphURL} + style={{ position: "absolute", top: 0, left: 0 }} + width="100%" + height="100%" + ></iframe> + </div> + <SubscriberTable {...data} /> + </> + ); } async function getData() { - const apiUrl = process.env.NEXT_PUBLIC_API_URL_TESTING - const response = await fetch(apiUrl + '/api/subscribers', { - headers: { - 'Cache-Control': 'no-cache' - }, - cache: 'no-cache' - }); - if (!response.ok) { - console.log(response.statusText); - } - return response.json(); + const apiUrl = process.env.NEXT_PUBLIC_API_URL_TESTING; + const response = await fetch(apiUrl + "/api/subscribers", { + headers: { + "Cache-Control": "no-cache", + }, + cache: "no-cache", + }); + if (!response.ok) { + console.log(response.statusText); + } + return response.json(); } -export default Home;
\ No newline at end of file +export default Home; diff --git a/src/components/CompactTable/CompactTable.tsx b/src/components/CompactTable/CompactTable.tsx index 64eb4f2..6ec6b7c 100644 --- a/src/components/CompactTable/CompactTable.tsx +++ b/src/components/CompactTable/CompactTable.tsx @@ -1,36 +1,45 @@ -import React from 'react'; +import type React from "react"; interface CompactTableProps { - tableData: { - dates: string[]; - milestones: string[]; - } - + tableData: { + dates: string[]; + milestones: string[]; + }; } const CompactTable: React.FC<CompactTableProps> = ({ tableData }) => { - return ( - <div className="max-w-full mx-auto bg-gray-100 shadow-md rounded-lg overflow-hidden"> - <div className="flex gap-x-4"> - <div className="w-1/2 px-4 py-5"> - <h2 className="text-lg font-semibold text-gray-900">Dates</h2> - <ul className="mt-3"> - {tableData.dates.map((date, index) => ( - <li key={index} className="text-gray-700 text-sm py-1 border-b border-gray-200">{date}</li> - ))} - </ul> - </div> - <div className="w-1/2 px-4 py-5"> - <h2 className="text-lg font-semibold text-gray-900">Milestones</h2> - <ul className="mt-3"> - {tableData.milestones.map((milestone, index) => ( - <li key={index} className="text-gray-700 text-sm py-1 border-b border-gray-200">{milestone.toLocaleString()}</li> - ))} - </ul> - </div> - </div> - </div> - ); + return ( + <div className="max-w-full mx-auto bg-gray-100 shadow-md rounded-lg overflow-hidden"> + <div className="flex gap-x-4"> + <div className="w-1/2 px-4 py-5"> + <h2 className="text-lg font-semibold text-gray-900">Dates</h2> + <ul className="mt-3"> + {tableData.dates.map((date, index) => ( + <li + key={index} + className="text-gray-700 text-sm py-1 border-b border-gray-200" + > + {date} + </li> + ))} + </ul> + </div> + <div className="w-1/2 px-4 py-5"> + <h2 className="text-lg font-semibold text-gray-900">Milestones</h2> + <ul className="mt-3"> + {tableData.milestones.map((milestone, index) => ( + <li + key={index} + className="text-gray-700 text-sm py-1 border-b border-gray-200" + > + {milestone.toLocaleString()} + </li> + ))} + </ul> + </div> + </div> + </div> + ); }; export default CompactTable; diff --git a/src/components/DataChart/DataChart.tsx b/src/components/DataChart/DataChart.tsx index 4f8eecd..3b8304a 100644 --- a/src/components/DataChart/DataChart.tsx +++ b/src/components/DataChart/DataChart.tsx @@ -1,82 +1,82 @@ -import React from "react"; import { - Chart as ChartJS, - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend, + CategoryScale, + Chart as ChartJS, + Legend, + LineElement, + LinearScale, + PointElement, + Title, + Tooltip, } from "chart.js"; +import type React from "react"; import { Line } from "react-chartjs-2"; ChartJS.register( - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, ); interface DataChartProps { - chartData?: any; - graphTitle?: string; - fullData?: boolean; - overrideBorderColor?: string - overrideBGColor?: string + chartData?: any; + graphTitle?: string; + fullData?: boolean; + overrideBorderColor?: string; + overrideBGColor?: string; } const DataChart: React.FC<DataChartProps> = ({ - chartData, - graphTitle, - fullData, - overrideBGColor, - overrideBorderColor + chartData, + graphTitle, + fullData, + overrideBGColor, + overrideBorderColor, }) => { - const options = { - responsive: true, - plugins: { - legend: { - position: "top" as const, - }, - title: { - display: true, - text: graphTitle || "Historical Subscriber Data", - font: { - size: 18, - }, - }, - }, - scales: { - x: { - ticks: { - autoSkip: true, - maxTicksLimit: 10, - }, - }, - }, - }; + const options = { + responsive: true, + plugins: { + legend: { + position: "top" as const, + }, + title: { + display: true, + text: graphTitle || "Historical Subscriber Data", + font: { + size: 18, + }, + }, + }, + scales: { + x: { + ticks: { + autoSkip: true, + maxTicksLimit: 10, + }, + }, + }, + }; - const data = { - labels: chartData.labels, - datasets: [ - { - label: "Subscriber Count", - data: chartData.datasets, - borderColor: overrideBorderColor||"rgb(255, 99, 132)", - backgroundColor: overrideBGColor||"rgba(255, 99, 132, 0.5)", - }, - ], - }; + const data = { + labels: chartData.labels, + datasets: [ + { + label: "Subscriber Count", + data: chartData.datasets, + borderColor: overrideBorderColor || "rgb(255, 99, 132)", + backgroundColor: overrideBGColor || "rgba(255, 99, 132, 0.5)", + }, + ], + }; - if (!fullData) { - return <Line options={options} data={data} />; - } else { - return <Line options={options} data={chartData} />; - } + if (!fullData) { + return <Line options={options} data={data} />; + } else { + return <Line options={options} data={chartData} />; + } }; export default DataChart; diff --git a/src/components/Divider/Divider.tsx b/src/components/Divider/Divider.tsx index 55e6844..e2ef41b 100644 --- a/src/components/Divider/Divider.tsx +++ b/src/components/Divider/Divider.tsx @@ -1,12 +1,14 @@ interface DividerProps { - text: string; + text: string; } const Divider = (props: DividerProps) => { - return ( - <div className="flex flex-row items-center justify-center bg-black h-24 mt-8"> - <div className="px-2 text-white text-4xl font-extrabold">{props.text}</div> - </div> - ) -} -export default Divider;
\ No newline at end of file + return ( + <div className="flex flex-row items-center justify-center bg-black h-24 mt-8"> + <div className="px-2 text-white text-4xl font-extrabold"> + {props.text} + </div> + </div> + ); +}; +export default Divider; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index 22573cd..8686e4c 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -1,26 +1,32 @@ - -import React from 'react'; +import React from "react"; const Footer = () => { - return ( - <footer> - <div className="text-center mt-4"> - <p className="font-bold"> - Information - </p> - <p className="text-m"> - Information is collected once per hour. Data collection will stop upon graduation - <br/> - This page is in no way affiliated with Phase Connect or with any of the channels listed here. - <br/> - Data Collection Started: 2022-04-01 (Earlier data may not be fully accurate) - </p> - <p className="p-4"> - <a className="hover:underline text-bold" href="https://github.com/pinapelz/Nijitrack">Source Code</a><br/> - </p> - </div> - </footer> - ); + return ( + <footer> + <div className="text-center mt-4"> + <p className="font-bold">Information</p> + <p className="text-m"> + Information is collected once per hour. Data collection will stop upon + graduation + <br /> + This page is in no way affiliated with Phase Connect or with any of + the channels listed here. + <br /> + Data Collection Started: 2022-04-01 (Earlier data may not be fully + accurate) + </p> + <p className="p-4"> + <a + className="hover:underline text-bold" + href="https://github.com/pinapelz/Nijitrack" + > + Source Code + </a> + <br /> + </p> + </div> + </footer> + ); }; export default Footer; diff --git a/src/components/SubscriberTable/SubscriberTable.tsx b/src/components/SubscriberTable/SubscriberTable.tsx index b07c75d..05080ca 100644 --- a/src/components/SubscriberTable/SubscriberTable.tsx +++ b/src/components/SubscriberTable/SubscriberTable.tsx @@ -2,67 +2,82 @@ import React from "react"; import ChannelRow from "./SubscriberTableRow"; interface ChannelDataProp { - channel_name: string; - profile_pic: string; - subscribers: number; - sub_org: string; - video_count: number; - day_diff: number; - views: number; + channel_name: string; + profile_pic: string; + subscribers: number; + sub_org: string; + video_count: number; + day_diff: number; + views: number; } interface SubscriberDataTableProp { - channel_data: ChannelDataProp[]; - timestamp: string; + channel_data: ChannelDataProp[]; + timestamp: string; } const DataTable = ({ channel_data, timestamp }: SubscriberDataTableProp) => { - if (!channel_data) { - return null; - } + if (!channel_data) { + return null; + } -return ( - <> - <div className="text-center sm:mt-5"> - <h1 className="text-2xl font-bold text-gray-800">Subscriber Count</h1> - <p className="text-gray-500 text-sm">Last Updated: {timestamp}</p> - </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" style={{ backgroundColor: 'black' }}> - <tr> - <th scope="col" className="py-1 px-1 sm:px-3 hidden sm:table-cell"> - RANK - </th> - <th scope="col" className="py-1 px-1 sm:px-3"> - CHANNEL - </th> - <th scope="col" className="py-1 px-1 sm:px-3 hidden sm:table-cell"> - GROUP - </th> - <th scope="col" className="py-1 px-1 sm:px-3 hidden sm:table-cell"> - VIDEO COUNT - </th> - <th scope="col" className="py-1 px-1 sm:px-3 hidden sm:table-cell"> - VIEW COUNT - </th> - <th scope="col" className="py-1 px-1 sm:px-3"> - SUBSCRIBERS - </th> - <th scope="col" className="py-1 px-1 sm:px-3"> - DIFF (24H) - </th> - </tr> - </thead> - <tbody> - {channel_data.map((channel, index) => ( - <ChannelRow key={index} channel={channel} index={index} /> - ))} - </tbody> - </table> - </div> - </> -); + return ( + <> + <div className="text-center sm:mt-5"> + <h1 className="text-2xl font-bold text-gray-800">Subscriber Count</h1> + <p className="text-gray-500 text-sm">Last Updated: {timestamp}</p> + </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" + style={{ backgroundColor: "black" }} + > + <tr> + <th + scope="col" + className="py-1 px-1 sm:px-3 hidden sm:table-cell" + > + RANK + </th> + <th scope="col" className="py-1 px-1 sm:px-3"> + CHANNEL + </th> + <th + scope="col" + className="py-1 px-1 sm:px-3 hidden sm:table-cell" + > + GROUP + </th> + <th + scope="col" + className="py-1 px-1 sm:px-3 hidden sm:table-cell" + > + VIDEO COUNT + </th> + <th + scope="col" + className="py-1 px-1 sm:px-3 hidden sm:table-cell" + > + VIEW COUNT + </th> + <th scope="col" className="py-1 px-1 sm:px-3"> + SUBSCRIBERS + </th> + <th scope="col" className="py-1 px-1 sm:px-3"> + DIFF (24H) + </th> + </tr> + </thead> + <tbody> + {channel_data.map((channel, index) => ( + <ChannelRow key={index} channel={channel} index={index} /> + ))} + </tbody> + </table> + </div> + </> + ); }; export default DataTable; diff --git a/src/components/SubscriberTable/SubscriberTableRow.tsx b/src/components/SubscriberTable/SubscriberTableRow.tsx index 040c693..595a2c1 100644 --- a/src/components/SubscriberTable/SubscriberTableRow.tsx +++ b/src/components/SubscriberTable/SubscriberTableRow.tsx @@ -1,36 +1,48 @@ -"use client" -import React from 'react'; -import Image from 'next/image'; -import { ChannelDataProp } from './SubscriberTable'; +"use client"; +import Image from "next/image"; +import type React from "react"; +import type { ChannelDataProp } from "./SubscriberTable"; interface ChannelRowProps { - channel: ChannelDataProp; - index: number; + channel: ChannelDataProp; + index: number; } const ChannelRow: React.FC<ChannelRowProps> = ({ 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"> - {channel.channel_name} - </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 hidden sm:table-cell">{channel.video_count}</td> - <td className="py-3 px-1 sm:px-3 hidden sm:table-cell">{Number(channel.views).toLocaleString()}</td> - <td className="py-3 px-1 sm:px-3">{Number(channel.subscribers).toLocaleString()}</td> - <td className="py-3 px-1 sm:px-3"> - {channel.day_diff > 0 ? `+${Number(channel.day_diff).toLocaleString()}` : Number(channel.day_diff).toLocaleString()} - </td> - </tr> + <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">{channel.channel_name}</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 hidden sm:table-cell"> + {channel.video_count} + </td> + <td className="py-3 px-1 sm:px-3 hidden sm:table-cell"> + {Number(channel.views).toLocaleString()} + </td> + <td className="py-3 px-1 sm:px-3"> + {Number(channel.subscribers).toLocaleString()} + </td> + <td className="py-3 px-1 sm:px-3"> + {channel.day_diff > 0 + ? `+${Number(channel.day_diff).toLocaleString()}` + : Number(channel.day_diff).toLocaleString()} + </td> + </tr> ); -export default ChannelRow;
\ No newline at end of file +export default ChannelRow; diff --git a/src/components/TitleBar/TitleBar.tsx b/src/components/TitleBar/TitleBar.tsx index 171e03a..127534e 100644 --- a/src/components/TitleBar/TitleBar.tsx +++ b/src/components/TitleBar/TitleBar.tsx @@ -1,32 +1,51 @@ -import React from 'react'; -import '../TitleBar/TitleBarStyle.css' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faHouse } from '@fortawesome/free-solid-svg-icons' +import type React from "react"; +import "../TitleBar/TitleBarStyle.css"; +import { faHouse } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; interface TitleBarProps { - title: string; - redirectUrl?: string; - showHomeButton?: boolean; - backgroundColor?: string; + title: string; + redirectUrl?: string; + showHomeButton?: boolean; + backgroundColor?: string; } -const TitleBar: React.FC<TitleBarProps> = ({ title, redirectUrl, showHomeButton, backgroundColor }) => { - return ( - <> - <div className="title-bar p-5 shadow-md" style={{ backgroundColor: backgroundColor || '#2D4B71' }}> - <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> - <a href={redirectUrl}> - <span className="text-white text-4xl font-bold" style={{ fontFamily: 'Quantico, sans-serif' }}>{title}</span> - </a> - {showHomeButton && ( - <a href="/" className='text-white text-3xl'> - <FontAwesomeIcon icon={faHouse} /> - </a> - )} - </div> - </div> - </> - ); +const TitleBar: React.FC<TitleBarProps> = ({ + title, + redirectUrl, + showHomeButton, + backgroundColor, +}) => { + return ( + <> + <div + className="title-bar p-5 shadow-md" + style={{ backgroundColor: backgroundColor || "#2D4B71" }} + > + <div + style={{ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + }} + > + <a href={redirectUrl}> + <span + className="text-white text-4xl font-bold" + style={{ fontFamily: "Quantico, sans-serif" }} + > + {title} + </span> + </a> + {showHomeButton && ( + <a href="/" className="text-white text-3xl"> + <FontAwesomeIcon icon={faHouse} /> + </a> + )} + </div> + </div> + </> + ); }; -export default TitleBar;
\ No newline at end of file +export default TitleBar; diff --git a/src/components/channel-card.tsx b/src/components/channel-card.tsx index b66a6e0..c19f492 100644 --- a/src/components/channel-card.tsx +++ b/src/components/channel-card.tsx @@ -1,59 +1,84 @@ -import { AvatarImage, AvatarFallback, Avatar } from "@/components/ui/avatar" -import { CardTitle, CardHeader, CardContent, Card } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; interface ChannelCardProps { - channel_id: string - name: string - avatarUrl: string - subscriberCount: number - videoCount: number - viewCount: number - suborg: string - nextMilestone: string - nextMilestoneDays: string - nextMilestoneDate: string + channel_id: string; + name: string; + avatarUrl: string; + subscriberCount: number; + videoCount: number; + viewCount: number; + suborg: string; + nextMilestone: string; + nextMilestoneDays: string; + nextMilestoneDate: string; } export function ChannelCard(props: ChannelCardProps) { - const { channel_id, name, avatarUrl, subscriberCount, videoCount, viewCount, suborg, nextMilestone, nextMilestoneDays, nextMilestoneDate } = props - return ( - <Card className="w-[500px] shadow-lg rounded-lg overflow-hidden mt-4 py-4"> - <CardHeader> - <div className="flex items-center space-x-4 p-4"> - <Avatar> - <AvatarImage src={avatarUrl}/> - <AvatarFallback>PR</AvatarFallback> - </Avatar> - <div> - <a className="hover:underline" href={`https://youtube.com/channel/${channel_id}`}><CardTitle>{name}</CardTitle></a> - <Badge variant="secondary">{suborg}</Badge> - </div> - </div> - </CardHeader> - <CardContent className="px-4 py-2 space-y-4"> - <div className="flex flex-col items-center"> - <span className="text-l text-gray-600">Subscribers</span> - <span className="font-semibold">{Number(subscriberCount).toLocaleString()}</span> - </div> - <div className="flex flex-col items-center"> - <span className="text-l text-gray-600">Videos</span> - <span className="font-semibold">{videoCount}</span> - </div> - <div className="flex flex-col items-center"> - <span className="text-l text-gray-600">View Count</span> - <span className="font-semibold">{Number(viewCount).toLocaleString()}</span> - </div> - <div className="flex flex-col items-center"> - <span className="text-l text-gray-600">Next Milestone</span> - <span className="font-semibold">{Number(nextMilestone).toLocaleString()}</span> - <div className="flex justify-center items-center"> - <span className="text-sm text-gray-600 px-2">{nextMilestoneDays} days</span> - <span className="text-sm text-gray-600 px-2">{nextMilestoneDate}</span> - </div> - - </div> - </CardContent> - </Card> - ) + const { + channel_id, + name, + avatarUrl, + subscriberCount, + videoCount, + viewCount, + suborg, + nextMilestone, + nextMilestoneDays, + nextMilestoneDate, + } = props; + return ( + <Card className="w-[500px] shadow-lg rounded-lg overflow-hidden mt-4 py-4"> + <CardHeader> + <div className="flex items-center space-x-4 p-4"> + <Avatar> + <AvatarImage src={avatarUrl} /> + <AvatarFallback>PR</AvatarFallback> + </Avatar> + <div> + <a + className="hover:underline" + href={`https://youtube.com/channel/${channel_id}`} + > + <CardTitle>{name}</CardTitle> + </a> + <Badge variant="secondary">{suborg}</Badge> + </div> + </div> + </CardHeader> + <CardContent className="px-4 py-2 space-y-4"> + <div className="flex flex-col items-center"> + <span className="text-l text-gray-600">Subscribers</span> + <span className="font-semibold"> + {Number(subscriberCount).toLocaleString()} + </span> + </div> + <div className="flex flex-col items-center"> + <span className="text-l text-gray-600">Videos</span> + <span className="font-semibold">{videoCount}</span> + </div> + <div className="flex flex-col items-center"> + <span className="text-l text-gray-600">View Count</span> + <span className="font-semibold"> + {Number(viewCount).toLocaleString()} + </span> + </div> + <div className="flex flex-col items-center"> + <span className="text-l text-gray-600">Next Milestone</span> + <span className="font-semibold"> + {Number(nextMilestone).toLocaleString()} + </span> + <div className="flex justify-center items-center"> + <span className="text-sm text-gray-600 px-2"> + {nextMilestoneDays} days + </span> + <span className="text-sm text-gray-600 px-2"> + {nextMilestoneDate} + </span> + </div> + </div> + </CardContent> + </Card> + ); } diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 1c69f50..3c884e8 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -1,50 +1,50 @@ -"use client" +"use client"; -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as AvatarPrimitive from "@radix-ui/react-avatar"; +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Avatar = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> + React.ElementRef<typeof AvatarPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> >(({ className, ...props }, ref) => ( - <AvatarPrimitive.Root - ref={ref} - className={cn( - "relative flex h-12 w-12 overflow-hidden rounded-full", - className - )} - {...props} - /> -)) -Avatar.displayName = AvatarPrimitive.Root.displayName + <AvatarPrimitive.Root + ref={ref} + className={cn( + "relative flex h-12 w-12 overflow-hidden rounded-full", + className, + )} + {...props} + /> +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; const AvatarImage = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Image>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> + React.ElementRef<typeof AvatarPrimitive.Image>, + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> >(({ className, ...props }, ref) => ( - <AvatarPrimitive.Image - ref={ref} - className={cn("aspect-square h-full w-full", className)} - {...props} - /> -)) -AvatarImage.displayName = AvatarPrimitive.Image.displayName + <AvatarPrimitive.Image + ref={ref} + className={cn("aspect-square h-full w-full", className)} + {...props} + /> +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; const AvatarFallback = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Fallback>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> + React.ElementRef<typeof AvatarPrimitive.Fallback>, + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> >(({ className, ...props }, ref) => ( - <AvatarPrimitive.Fallback - ref={ref} - className={cn( - "flex h-full w-full items-center justify-center rounded-full bg-muted", - className - )} - {...props} - /> -)) -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + <AvatarPrimitive.Fallback + ref={ref} + className={cn( + "flex h-full w-full items-center justify-center rounded-full bg-muted", + className, + )} + {...props} + /> +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; -export { Avatar, AvatarImage, AvatarFallback } +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index f000e3e..f38976c 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -1,36 +1,36 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import { type VariantProps, cva } from "class-variance-authority"; +import type * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); export interface BadgeProps - extends React.HTMLAttributes<HTMLDivElement>, - VariantProps<typeof badgeVariants> {} + extends React.HTMLAttributes<HTMLDivElement>, + VariantProps<typeof badgeVariants> {} function Badge({ className, variant, ...props }: BadgeProps) { - return ( - <div className={cn(badgeVariants({ variant }), className)} {...props} /> - ) + return ( + <div className={cn(badgeVariants({ variant }), className)} {...props} /> + ); } -export { Badge, badgeVariants } +export { Badge, badgeVariants }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 0f7034b..2681236 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,79 +1,86 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn( - "rounded-lg border bg-card text-card-foreground shadow-sm", - className - )} - {...props} - /> -)) -Card.displayName = "Card" + <div + ref={ref} + className={cn( + "rounded-lg border bg-card text-card-foreground shadow-sm", + className, + )} + {...props} + /> +)); +Card.displayName = "Card"; const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("flex flex-col space-y-1 px-4", className)} - {...props} - /> -)) -CardHeader.displayName = "CardHeader" + <div + ref={ref} + className={cn("flex flex-col space-y-1 px-4", className)} + {...props} + /> +)); +CardHeader.displayName = "CardHeader"; const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLHeadingElement> + HTMLParagraphElement, + React.HTMLAttributes<HTMLHeadingElement> >(({ className, ...props }, ref) => ( - <h3 - ref={ref} - className={cn( - "text-2xl font-semibold leading-none tracking-tight", - className - )} - {...props} - /> -)) -CardTitle.displayName = "CardTitle" + <h3 + ref={ref} + className={cn( + "text-2xl font-semibold leading-none tracking-tight", + className, + )} + {...props} + /> +)); +CardTitle.displayName = "CardTitle"; const CardDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLParagraphElement> + HTMLParagraphElement, + React.HTMLAttributes<HTMLParagraphElement> >(({ className, ...props }, ref) => ( - <p - ref={ref} - className={cn("text-sm text-muted-foreground", className)} - {...props} - /> -)) -CardDescription.displayName = "CardDescription" + <p + ref={ref} + className={cn("text-sm text-muted-foreground", className)} + {...props} + /> +)); +CardDescription.displayName = "CardDescription"; const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => ( - <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> -)) -CardContent.displayName = "CardContent" + <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> +)); +CardContent.displayName = "CardContent"; const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("flex items-center p-6 pt-0", className)} - {...props} - /> -)) -CardFooter.displayName = "CardFooter" + <div + ref={ref} + className={cn("flex items-center p-6 pt-0", className)} + {...props} + /> +)); +CardFooter.displayName = "CardFooter"; -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index d084cca..ac680b3 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,6 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } diff --git a/src/pages/stats/[slug].tsx b/src/pages/stats/[slug].tsx index 224046f..5fc76cb 100644 --- a/src/pages/stats/[slug].tsx +++ b/src/pages/stats/[slug].tsx @@ -1,134 +1,207 @@ -import { GetServerSideProps } from "next"; +import type { GetServerSideProps } from "next"; import "../../app/globals.css"; -import TitleBar from "../../components/TitleBar/TitleBar"; -import { ChannelCard } from "@/components/channel-card"; +import CompactTable from "@/components/CompactTable/CompactTable"; import DataChart from "@/components/DataChart/DataChart"; -import Footer from "@/components/Footer/Footer"; -import Head from 'next/head' import Divider from "@/components/Divider/Divider"; +import Footer from "@/components/Footer/Footer"; +import { ChannelCard } from "@/components/channel-card"; +import Head from "next/head"; +import TitleBar from "../../components/TitleBar/TitleBar"; interface ChannelDataProp { - channel_id: string; - channel_name: string; - profile_pic: string; - subscribers: number; - sub_org: string; - video_count: number; - view_count: number; - next_milestone: string; - days_until_next_milestone: string; - next_milestone_date: string; + channel_id: string; + channel_name: string; + profile_pic: string; + subscribers: number; + sub_org: string; + video_count: number; + view_count: number; + next_milestone: string; + days_until_next_milestone: string; + next_milestone_date: string; +} + +interface GraphDataProp { + labels: string[]; + datasets: number[]; } -interface GraphDataProp{ - labels: string[]; - datasets: number[]; +interface CompactTableProps { + dates: string[]; + milestones: string[]; } export const getServerSideProps: GetServerSideProps = async (context) => { - const { slug } = context.params || {}; + const { slug } = context.params || {}; - const chartData = await getGraphData(slug as string); - const channelData = await getChannelData(slug as string); - const sevenDayGraphData = await get7DGraphData(slug as string); + const chartData = await getGraphData(slug as string); + const channelData = await getChannelData(slug as string); + const sevenDayGraphData = await get7DGraphData(slug as string); + const milestoneData = await getMilestoneData(slug as string); - return { - props: { - chartData, - channelData, - slug, - sevenDayGraphData - }, - }; + return { + props: { + chartData, + channelData, + slug, + sevenDayGraphData, + milestoneData, + }, + }; }; -function Page({ chartData, channelData, sevenDayGraphData, slug }: { chartData: GraphDataProp, channelData: ChannelDataProp, sevenDayGraphData: GraphDataProp, slug: string }) { - return ( - <> - <Head> - <title>{slug as string} - PhaseTracker</title> - <meta property="og:title" content={`${slug as string} - PhaseTracker`}/> - <meta name="description" content={`Belonging to ${channelData.sub_org} with ${channelData.subscribers} subscribers`} /> - <meta name="og:description" content={`${channelData.sub_org} - ${channelData.subscribers}`} /> - <meta property="og:image" content={`${channelData.profile_pic}`}/> - <meta name="viewport" content="width=device-width, initial-scale=1.0"></meta> - <meta name="author" content="Pinapelz"></meta> - </Head> - <TitleBar title={slug as string} redirectUrl="/" showHomeButton backgroundColor="black" /> - <div className="flex justify-center"> - <div className="flex flex-col items-center"> - <ChannelCard - channel_id={channelData.channel_id} - name={channelData.channel_name} - avatarUrl={channelData.profile_pic} - subscriberCount={channelData.subscribers} - videoCount={channelData.video_count} - viewCount={channelData.view_count} - suborg={channelData.sub_org} - nextMilestone={channelData.next_milestone} - nextMilestoneDays={channelData.days_until_next_milestone} - nextMilestoneDate={channelData.next_milestone_date} - /> - </div> - </div> - <Divider text="Individual Data"/> - <div className="px-48 mb-10 mt-10"> - <div className="mb-12"> - <DataChart overrideBGColor="black" overrideBorderColor="black" chartData={chartData}/> - </div> - <div className="mb-12"> - <DataChart chartData={sevenDayGraphData} overrideBGColor="black" overrideBorderColor="black" graphTitle="7 Day Historical"/> - </div> - </div> - <Footer /> - </> - ); +function Page({ + chartData, + channelData, + sevenDayGraphData, + slug, + milestoneData, +}: { + chartData: GraphDataProp; + channelData: ChannelDataProp; + sevenDayGraphData: GraphDataProp; + slug: string; + milestoneData: CompactTableProps; +}) { + console.log(milestoneData); + return ( + <> + <Head> + <title>{slug as string} - PhaseTracker</title> + <meta + property="og:title" + content={`${slug as string} - PhaseTracker`} + /> + <meta + name="description" + content={`Belonging to ${channelData.sub_org} with ${channelData.subscribers} subscribers`} + /> + <meta + name="og:description" + content={`${channelData.sub_org} - ${channelData.subscribers}`} + /> + <meta property="og:image" content={`${channelData.profile_pic}`} /> + <meta + name="viewport" + content="width=device-width, initial-scale=1.0" + ></meta> + <meta name="author" content="Pinapelz"></meta> + </Head> + <TitleBar + title={slug as string} + redirectUrl="/" + showHomeButton + backgroundColor="black" + /> + <div className="flex justify-center"> + <div className="flex flex-col items-center"> + <ChannelCard + channel_id={channelData.channel_id} + name={channelData.channel_name} + avatarUrl={channelData.profile_pic} + subscriberCount={channelData.subscribers} + videoCount={channelData.video_count} + viewCount={channelData.view_count} + suborg={channelData.sub_org} + nextMilestone={channelData.next_milestone} + nextMilestoneDays={channelData.days_until_next_milestone} + nextMilestoneDate={channelData.next_milestone_date} + /> + </div> + </div> + <Divider text="Individual Data" /> + <div className="px-48 mb-10 mt-10"> + <div className="mb-12"> + <DataChart + overrideBGColor="black" + overrideBorderColor="black" + chartData={chartData} + /> + </div> + <div className="mb-12"> + <DataChart + chartData={sevenDayGraphData} + overrideBGColor="black" + overrideBorderColor="black" + graphTitle="7 Day Historical" + /> + </div> + <Divider text="Milestones" /> + <div className="mb-12"> + <CompactTable + tableData={{ + dates: milestoneData.dates, + milestones: milestoneData.milestones, + }} + /> + </div> + </div> + <Footer /> + </> + ); } -async function getGraphData(slug: string){ - const encodedSlug = encodeURIComponent(slug as string); - const apiUrl = process.env.NEXT_PUBLIC_API_URL - const response = await fetch(apiUrl+`/api/subscribers/${encodedSlug}`, { - headers: { - 'Cache-Control': 'no-cache' - }, - cache: 'no-cache' - }); - if(!response.ok){ - console.log(response.statusText); - } - return response.json(); +async function getGraphData(slug: string) { + const encodedSlug = encodeURIComponent(slug as string); + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + const response = await fetch(apiUrl + `/api/subscribers/${encodedSlug}`, { + headers: { + "Cache-Control": "no-cache", + }, + cache: "no-cache", + }); + if (!response.ok) { + console.log(response.statusText); + } + return response.json(); } -async function getChannelData(slug: string){ - const encodedSlug = encodeURIComponent(slug as string); - const apiUrl = process.env.NEXT_PUBLIC_API_URL - const response = await fetch(apiUrl+`/api/channel/${encodedSlug}`, { - headers: { - 'Cache-Control': 'no-cache' - }, - cache: 'no-cache' - }); - if(!response.ok){ - console.log(response.statusText); - } - return response.json(); +async function getChannelData(slug: string) { + const encodedSlug = encodeURIComponent(slug as string); + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + const response = await fetch(apiUrl + `/api/channel/${encodedSlug}`, { + headers: { + "Cache-Control": "no-cache", + }, + cache: "no-cache", + }); + if (!response.ok) { + console.log(response.statusText); + } + return response.json(); } -async function get7DGraphData(slug: string){ - const encodedSlug = encodeURIComponent(slug as string); - const apiUrl = process.env.NEXT_PUBLIC_API_URL - const response = await fetch(apiUrl+`/api/subscribers/${encodedSlug}/7d`, { - headers: { - 'Cache-Control': 'no-cache' - }, - cache: 'no-cache' - }); - if(!response.ok){ - console.log(response.statusText); - } - return response.json(); +async function get7DGraphData(slug: string) { + const encodedSlug = encodeURIComponent(slug as string); + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + const response = await fetch(apiUrl + `/api/subscribers/${encodedSlug}/7d`, { + headers: { + "Cache-Control": "no-cache", + }, + cache: "no-cache", + }); + if (!response.ok) { + console.log(response.statusText); + } + return response.json(); } +async function getMilestoneData(slug: string) { + const encodedSlug = encodeURIComponent(slug as string); + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + const response = await fetch( + apiUrl + `/api/subscribers/${encodedSlug}/milestones`, + { + headers: { + "Cache-Control": "no-cache", + }, + cache: "no-cache", + }, + ); + if (!response.ok) { + console.log(response.statusText); + } + return response.json(); +} export default Page; |
