diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/DataChart/DataChart.tsx | 108 | ||||
| -rw-r--r-- | src/components/Footer/Footer.tsx | 26 | ||||
| -rw-r--r-- | src/components/SubscriberTable/SubscriberTable.tsx | 66 | ||||
| -rw-r--r-- | src/components/SubscriberTable/SubscriberTableRow.tsx | 35 | ||||
| -rw-r--r-- | src/components/TitleBar/TitleBar.tsx | 26 | ||||
| -rw-r--r-- | src/components/channel-card.tsx | 53 | ||||
| -rw-r--r-- | src/components/ui/avatar.tsx | 50 | ||||
| -rw-r--r-- | src/components/ui/badge.tsx | 36 | ||||
| -rw-r--r-- | src/components/ui/card.tsx | 79 |
9 files changed, 479 insertions, 0 deletions
diff --git a/src/components/DataChart/DataChart.tsx b/src/components/DataChart/DataChart.tsx new file mode 100644 index 0000000..b51d592 --- /dev/null +++ b/src/components/DataChart/DataChart.tsx @@ -0,0 +1,108 @@ + + +import React, {useEffect, useState} from 'react'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +} from 'chart.js'; +import { Line } from 'react-chartjs-2'; + + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend +); + + + + + +interface Dataset { + label: string; + data: number[]; + borderColor: string; + backgroundColor: string; +} + +interface DataChartResponseProps { + labels: string[]; + datasets: Dataset[]; +} + +interface DataChartProps { + channel_name: string; + requestUrl?: string; + graphTitle?: string; +} + +const DataChart: React.FC<DataChartProps> = ({ channel_name, requestUrl, graphTitle }) => { + const [data, setData] = useState<DataChartResponseProps | null>(); + const apiUrl = process.env.NEXT_PUBLIC_API_URL + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch(requestUrl || `${apiUrl}/api/subscribers/${channel_name}`); + const json = await response.json(); + setData({ + labels: json.labels, + datasets: [ + { + label: 'Subscriber Count', + data: json.datasets, + borderColor: 'rgb(255, 99, 132)', + backgroundColor: 'rgba(255, 99, 132, 0.5)', + }, + ], + }); + } catch (error) { + console.error('Error fetching data:', error); + } + }; + + fetchData(); + }, [apiUrl, channel_name, requestUrl]); + + 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 + } + } + } + }; + + if (!data) { + return <div>Loading...</div>; + } + + return <Line options={options} data={data} />; +}; + +export default DataChart;
\ No newline at end of file diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 0000000..6585e52 --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,26 @@ + +import React from 'react'; + +const Footer = () => { + return ( + <footer> + <div className="text-center"> + <p className="text-bold"> + Information + </p> + <p className="text-m"> + Information is collected once per hour. Data collection will stop once a liver has graduated. + <br/> + This page is in now way affiliated with ANYCOLOR or with any of the channels listed here. + <br/> + Date Started: 2023-03-26 + </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 new file mode 100644 index 0000000..8094e21 --- /dev/null +++ b/src/components/SubscriberTable/SubscriberTable.tsx @@ -0,0 +1,66 @@ +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; +} + +interface SubscriberDataTableProp { + channel_data: ChannelDataProp[]; + timestamp: string; +} + +const DataTable = ({ channel_data, timestamp }: SubscriberDataTableProp) => { + 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 shadow-md 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" style={{ backgroundColor: '#2D4B71' }}> + <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"> + 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; +export type { SubscriberDataTableProp }; +export type { ChannelDataProp }; diff --git a/src/components/SubscriberTable/SubscriberTableRow.tsx b/src/components/SubscriberTable/SubscriberTableRow.tsx new file mode 100644 index 0000000..e97af1c --- /dev/null +++ b/src/components/SubscriberTable/SubscriberTableRow.tsx @@ -0,0 +1,35 @@ +"use client" +import React from 'react'; +import Image from 'next/image'; +import { ChannelDataProp } from './SubscriberTable'; + +interface ChannelRowProps { + 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">{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 diff --git a/src/components/TitleBar/TitleBar.tsx b/src/components/TitleBar/TitleBar.tsx new file mode 100644 index 0000000..85fbfbd --- /dev/null +++ b/src/components/TitleBar/TitleBar.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +interface TitleBarProps { + title: string; + redirectUrl?: string; + showHomeButton?: boolean; +} + +const TitleBar: React.FC<TitleBarProps> = ({ title, redirectUrl, showHomeButton }) => { + return ( + <div className="title-bar p-5 shadow-md" style={{ backgroundColor: '#2D4B71' }}> + <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> + <a href={redirectUrl}> + <span className="text-white text-4xl font-bold">{title}</span> + </a> + {showHomeButton && ( + <a href="/"> + <button className="bg-white text-blue-500 hover:bg-blue-500 hover:text-white font-bold py-2 px-4 rounded-full">Home</button> + </a> + )} + </div> + </div> + ); +}; + +export default TitleBar;
\ No newline at end of file diff --git a/src/components/channel-card.tsx b/src/components/channel-card.tsx new file mode 100644 index 0000000..f2eed59 --- /dev/null +++ b/src/components/channel-card.tsx @@ -0,0 +1,53 @@ +import { AvatarImage, AvatarFallback, Avatar } from "@/components/ui/avatar" +import { CardTitle, CardHeader, CardContent, Card } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" + +interface ChannelCardProps { + name: string + avatarUrl: string + subscriberCount: number + videoCount: number + suborg: string + nextMilestone: string + nextMilestoneDays: string + nextMilestoneDate: string +} + +export function ChannelCard(props: ChannelCardProps) { + const { name, avatarUrl, subscriberCount, videoCount, 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> + <CardTitle>{name}</CardTitle> + <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">{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">Next Milestone</span> + <span className="font-semibold">{nextMilestone}</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 new file mode 100644 index 0000000..1c69f50 --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + 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 + +const AvatarImage = React.forwardRef< + 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 + +const AvatarFallback = React.forwardRef< + 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 + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +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", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes<HTMLDivElement>, + VariantProps<typeof badgeVariants> {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( + <div className={cn(badgeVariants({ variant }), className)} {...props} /> + ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..0f7034b --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + 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" + +const CardHeader = React.forwardRef< + 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" + +const CardTitle = React.forwardRef< + 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" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes<HTMLParagraphElement> +>(({ className, ...props }, ref) => ( + <p + ref={ref} + className={cn("text-sm text-muted-foreground", className)} + {...props} + /> +)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> +>(({ className, ...props }, ref) => ( + <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> +)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> +>(({ className, ...props }, ref) => ( + <div + ref={ref} + className={cn("flex items-center p-6 pt-0", className)} + {...props} + /> +)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } |
