aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/DataChart/DataChart.tsx108
-rw-r--r--src/components/Footer/Footer.tsx26
-rw-r--r--src/components/SubscriberTable/SubscriberTable.tsx66
-rw-r--r--src/components/SubscriberTable/SubscriberTableRow.tsx35
-rw-r--r--src/components/TitleBar/TitleBar.tsx26
-rw-r--r--src/components/channel-card.tsx53
-rw-r--r--src/components/ui/avatar.tsx50
-rw-r--r--src/components/ui/badge.tsx36
-rw-r--r--src/components/ui/card.tsx79
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 }
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage