diff options
| author | Pinapelz <yukais@pinapelz.com> | 2024-10-10 01:29:18 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2024-10-10 01:29:18 -0700 |
| commit | bb57421e9878e8c00636c357afb80eff563ba263 (patch) | |
| tree | aa84f91e9bab6a9f0f56510d744ab4ad2507b597 /src | |
| parent | f8a3408ac1f521c4049de96b44637cf0b8c1028c (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.tsx | 210 | ||||
| -rw-r--r-- | src/components/Countdown.tsx | 117 | ||||
| -rw-r--r-- | src/pages/stats/[slug].tsx | 345 |
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; |
