aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2024-10-10 01:29:18 -0700
committerPinapelz <yukais@pinapelz.com>2024-10-10 01:29:18 -0700
commitbb57421e9878e8c00636c357afb80eff563ba263 (patch)
treeaa84f91e9bab6a9f0f56510d744ab4ad2507b597 /src
parentf8a3408ac1f521c4049de96b44637cf0b8c1028c (diff)
added diff historical to channel card, fix channel card CD alignment on sm devices
Diffstat (limited to 'src')
-rw-r--r--src/components/ChannelCard/ChannelCard.tsx210
-rw-r--r--src/components/Countdown.tsx117
-rw-r--r--src/pages/stats/[slug].tsx345
3 files changed, 366 insertions, 306 deletions
diff --git a/src/components/ChannelCard/ChannelCard.tsx b/src/components/ChannelCard/ChannelCard.tsx
index 1cdb03e..5337b26 100644
--- a/src/components/ChannelCard/ChannelCard.tsx
+++ b/src/components/ChannelCard/ChannelCard.tsx
@@ -1,98 +1,130 @@
-import React from 'react';
-import Image from 'next/image';
-import Countdown from '../Countdown';
+import React from "react";
+import Image from "next/image";
+import Countdown from "../Countdown";
type 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;
+ diff_1d: number;
+ diff_7d: number;
+ diff_30d: number;
};
const ChannelCard: React.FC<ChannelCardProps> = ({
- channel_id,
- name,
- avatarUrl,
- subscriberCount,
- videoCount,
- viewCount,
- suborg,
- nextMilestone,
- nextMilestoneDays,
- nextMilestoneDate,
+ channel_id,
+ name,
+ avatarUrl,
+ subscriberCount,
+ videoCount,
+ viewCount,
+ suborg,
+ nextMilestone,
+ nextMilestoneDays,
+ nextMilestoneDate,
+ diff_1d,
+ diff_7d,
+ diff_30d,
}) => {
- return (
- <div className="max-w-4xl w-full mb-4 mt-4 rounded-xl overflow-hidden shadow-lg bg-gradient-to-r from-gray-800 via-gray-900 to-gray-800 p-4 sm:p-8 hover:shadow-2xl transition-all duration-300">
- <div className="flex flex-col sm:flex-row items-center mb-6">
- <Image
- src={avatarUrl}
- alt={name}
- width={80}
- height={80}
- className="rounded-full border-4 border-white"
- />
- <div className="mt-4 sm:mt-0 sm:ml-6 text-center sm:text-left">
- <h3 className="text-xl sm:text-2xl font-bold text-white">
- {name}
- </h3>
- {suborg && (
- <p className="text-sm sm:text-md text-gray-400">
- {suborg}
- </p>
- )}
- </div>
- </div>
- <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 sm:gap-8 text-center mb-6">
- <div>
- <p className="text-lg sm:text-xl font-bold text-white">
- {subscriberCount.toLocaleString()}
- </p>
- <p className="text-xs sm:text-sm text-gray-400">
- Subscribers
- </p>
- </div>
- <div>
- <p className="text-lg sm:text-xl font-bold text-white">
- {videoCount.toLocaleString()}
- </p>
- <p className="text-xs sm:text-sm text-gray-400">
- Videos
- </p>
- </div>
- <div>
- <p className="text-lg sm:text-xl font-bold text-white">
- {viewCount.toLocaleString()}
- </p>
- <p className="text-xs sm:text-sm text-gray-400">
- Views
- </p>
- </div>
- </div>
- <div className="bg-gray-700 rounded-lg p-4 sm:p-0 mb-6">
- <p className="text-md sm:text-lg font-semibold text-white">
- Next Milestone: {Number(nextMilestone).toLocaleString()}
- </p>
- <p className="text-xs sm:text-sm text-gray-300">
- Estimated Date: {nextMilestoneDate}
- </p>
- <div className="flex justify-center sm:p-2">
- <Countdown targetDate={nextMilestoneDate} />
+ return (
+ <div className="max-w-4xl w-full mb-4 mt-4 rounded-xl overflow-hidden shadow-lg bg-gradient-to-r from-gray-800 via-gray-900 to-gray-800 p-4 sm:p-8 hover:shadow-2xl transition-all duration-300">
+ <div className="flex flex-col sm:flex-row items-center mb-6">
+ <Image
+ src={avatarUrl}
+ alt={name}
+ width={80}
+ height={80}
+ className="rounded-full border-4 border-white"
+ />
+ <div className="mt-4 sm:mt-0 sm:ml-6 text-center sm:text-left">
+ <h3 className="text-xl sm:text-2xl font-bold text-white">
+ {name}
+ </h3>
+ {suborg && (
+ <p className="text-sm sm:text-md text-gray-400">
+ {suborg}
+ </p>
+ )}
+ </div>
+ </div>
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 sm:gap-8 text-center mb-6">
+ <div>
+ <p className="text-lg sm:text-xl font-bold text-white">
+ {subscriberCount.toLocaleString()}
+ </p>
+ <p className="text-xs sm:text-sm text-gray-400">
+ Subscribers
+ </p>
+ </div>
+ <div>
+ <p className="text-lg sm:text-xl font-bold text-white">
+ {videoCount.toLocaleString()}
+ </p>
+ <p className="text-xs sm:text-sm text-gray-400">Videos</p>
+ </div>
+ <div>
+ <p className="text-lg sm:text-xl font-bold text-white">
+ {viewCount.toLocaleString()}
+ </p>
+ <p className="text-xs sm:text-sm text-gray-400">Views</p>
+ </div>
+ <div>
+ <p className="text-lg sm:text-xl font-bold text-white">
+ {diff_1d.toLocaleString()}
+ </p>
+ <p className="text-xs sm:text-sm text-gray-400">
+ 24 Hour Change
+ </p>
+ </div>
+ <div>
+ <p className="text-lg sm:text-xl font-bold text-white">
+ {diff_7d.toLocaleString()}
+ </p>
+ <p className="text-xs sm:text-sm text-gray-400">
+ 7 Day Change
+ </p>
+ </div>
+ <div>
+ <p className="text-lg sm:text-xl font-bold text-white">
+ {diff_30d.toLocaleString()}
+ </p>
+ <p className="text-xs sm:text-sm text-gray-400">
+ 30 Day Change
+ </p>
+ </div>
+ </div>
+ <div className="bg-gray-700 rounded-lg text-center p-4 sm:p-0 mb-6">
+ <p className="text-md sm:text-lg font-semibold text-white">
+ Next Milestone: {Number(nextMilestone).toLocaleString()}
+ </p>
+ <p className="text-xs sm:text-sm text-gray-300">
+ Estimated Date: {nextMilestoneDate}
+ </p>
+ <div className="flex justify-center sm:p-2">
+ <Countdown targetDate={nextMilestoneDate} />
+ </div>
+ </div>
+
+ <button
+ onClick={() =>
+ window.open(
+ `https://youtube.com/channel/${channel_id}`,
+ "_blank",
+ )
+ }
+ className="w-full bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-6 rounded-lg transition-all duration-200"
+ >
+ View Channel on YouTube
+ </button>
</div>
- </div>
- <button
- onClick={() => window.open(`https://youtube.com/channel/${channel_id}`, '_blank')}
- className="w-full bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-6 rounded-lg transition-all duration-200"
- >
- View Channel on YouTube
- </button>
- </div>
- );
+ );
};
-export default ChannelCard; \ No newline at end of file
+export default ChannelCard;
diff --git a/src/components/Countdown.tsx b/src/components/Countdown.tsx
index a7af166..51b11af 100644
--- a/src/components/Countdown.tsx
+++ b/src/components/Countdown.tsx
@@ -1,70 +1,77 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState } from "react";
interface CountdownProps {
- targetDate: string;
+ targetDate: string;
}
const Countdown: React.FC<CountdownProps> = ({ targetDate }) => {
- const calculateTimeLeft = () => {
- const difference = new Date(targetDate).getTime() - new Date().getTime();
- let timeLeft = {
- days: '0',
- hours: '0',
- minutes: '0',
- seconds: '0',
- };
+ const calculateTimeLeft = () => {
+ const difference =
+ new Date(targetDate).getTime() - new Date().getTime();
+ let timeLeft = {
+ days: "0",
+ hours: "0",
+ minutes: "0",
+ seconds: "0",
+ };
- if (difference > 0) {
- timeLeft = {
- days: Math.floor(difference / (1000 * 60 * 60 * 24)).toString(),
- hours: Math.floor((difference / (1000 * 60 * 60)) % 24).toString(),
- minutes: Math.floor((difference / 1000 / 60) % 60).toString(),
- seconds: Math.floor((difference / 1000) % 60).toString(),
- };
- }
+ if (difference > 0) {
+ timeLeft = {
+ days: Math.floor(difference / (1000 * 60 * 60 * 24)).toString(),
+ hours: Math.floor(
+ (difference / (1000 * 60 * 60)) % 24,
+ ).toString(),
+ minutes: Math.floor((difference / 1000 / 60) % 60).toString(),
+ seconds: Math.floor((difference / 1000) % 60).toString(),
+ };
+ }
- return timeLeft;
- };
+ return timeLeft;
+ };
- const [timeLeft, setTimeLeft] = useState({
- days: '--',
- hours: '--',
- minutes: '--',
- seconds: '--',
- });
+ const [timeLeft, setTimeLeft] = useState({
+ days: "--",
+ hours: "--",
+ minutes: "--",
+ seconds: "--",
+ });
- useEffect(() => {
- setTimeLeft(calculateTimeLeft());
+ useEffect(() => {
+ setTimeLeft(calculateTimeLeft());
- const timer = setInterval(() => {
- setTimeLeft(calculateTimeLeft());
- }, 1000);
+ const timer = setInterval(() => {
+ setTimeLeft(calculateTimeLeft());
+ }, 1000);
- return () => clearInterval(timer);
- }, [targetDate]);
+ return () => clearInterval(timer);
+ }, [targetDate]);
- return (
- <div className="bg-gray-700 text-white font-sans">
- <div className="flex gap-2 text-2xl font-bold">
- <div className="flex flex-col items-center">
- <div className="text-4xl sm:text-2xl">{timeLeft.days}</div>
- <div className="text-xs uppercase">Days</div>
- </div>
- <div className="flex flex-col items-center">
- <div className="text-4xl sm:text-2xl">{timeLeft.hours}</div>
- <div className="text-xs uppercase">Hours</div>
- </div>
- <div className="flex flex-col items-center">
- <div className="text-4xl sm:text-2xl">{timeLeft.minutes}</div>
- <div className="text-xs uppercase">Minutes</div>
- </div>
- <div className="flex flex-col items-center">
- <div className="text-4xl sm:text-2xl">{timeLeft.seconds}</div>
- <div className="text-xs uppercase">Seconds</div>
+ return (
+ <div className="bg-gray-700 text-white font-sans">
+ <div className="flex gap-2 text-2xl font-bold">
+ <div className="flex flex-col items-center">
+ <div className="text-2xl sm:text-4xl">{timeLeft.days}</div>
+ <div className="text-xs uppercase">Days</div>
+ </div>
+ <div className="flex flex-col items-center">
+ <div className="text-2xl sm:text-4xl">{timeLeft.hours}</div>
+ <div className="text-xs uppercase">Hours</div>
+ </div>
+ <div className="flex flex-col items-center">
+ <div className="text-2xl sm:text-4xl">
+ {timeLeft.minutes}
+ </div>
+ <div className="text-xs uppercase">Minutes</div>
+ </div>
+ <div className="flex flex-col items-center">
+ <div className="text-2xl sm:text-4xl">
+ {timeLeft.seconds}
+ </div>
+ <div className="text-xs uppercase">Seconds</div>
+ </div>
+ </div>
</div>
- </div>
- </div>
- );
+ );
};
-export default Countdown; \ No newline at end of file
+export default Countdown;
diff --git a/src/pages/stats/[slug].tsx b/src/pages/stats/[slug].tsx
index 82d2757..6e6534a 100644
--- a/src/pages/stats/[slug].tsx
+++ b/src/pages/stats/[slug].tsx
@@ -4,197 +4,218 @@ import CompactTable from "@/components/CompactTable/CompactTable";
import DataChart from "@/components/DataChart/DataChart";
import Divider from "@/components/Divider/Divider";
import Footer from "@/components/Footer/Footer";
-import ChannelCard from "@/components/ChannelCard/ChannelCard"
+import ChannelCard from "@/components/ChannelCard/ChannelCard";
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[];
+ labels: string[];
+ datasets: number[];
}
interface CompactTableProps {
- dates: string[];
- milestones: string[];
+ 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 milestoneData = await getMilestoneData(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,
- milestoneData,
- },
- };
+ return {
+ props: {
+ chartData,
+ channelData,
+ slug,
+ sevenDayGraphData,
+ milestoneData,
+ },
+ };
};
function Page({
- chartData,
- channelData,
- sevenDayGraphData,
- slug,
- milestoneData,
- }: {
- chartData: GraphDataProp;
- channelData: ChannelDataProp;
- sevenDayGraphData: GraphDataProp;
- slug: string;
- milestoneData: CompactTableProps;
- }) {
- 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 px-12">
- <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 className="hidden sm:block">
- <Divider text="Individual Data" description="Data before collection start date are not shown" />
- <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>
- <Divider text="Historical Milestones" description="Approximations are shown for milestones before data collection started" />
- <div className="mb-12 mx-24">
- <CompactTable
- tableData={{
- dates: milestoneData.dates,
- milestones: milestoneData.milestones,
- }}
- />
- </div>
- </div>
- <Footer />
- </>
- );
- }
+ chartData,
+ channelData,
+ sevenDayGraphData,
+ slug,
+ milestoneData,
+}: {
+ chartData: GraphDataProp;
+ channelData: ChannelDataProp;
+ sevenDayGraphData: GraphDataProp;
+ slug: string;
+ milestoneData: CompactTableProps;
+}) {
+ 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 px-12">
+ <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}
+ diff_1d={channelData.diff_1d}
+ diff_7d={channelData.diff_7d}
+ diff_30d={channelData.diff_30d}
+ />
+ </div>
+ <div className="hidden sm:block">
+ <Divider
+ text="Individual Data"
+ description="Data before collection start date are not shown"
+ />
+ <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>
+ <Divider
+ text="Historical Milestones"
+ description="Approximations are shown for milestones before data collection started"
+ />
+ <div className="mb-12 mx-24">
+ <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();
+ 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();
+ 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();
+ 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();
+ 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;
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage