aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2024-06-26 22:01:38 -0700
committerPinapelz <yukais@pinapelz.com>2024-06-26 22:01:38 -0700
commit7c3f1298095727fcacdc903fa79369d5624bf3df (patch)
tree45ec08396c69916971a60965c08255f7c8e0b613
parent5f58260a7602494f21969fd917a0a984ae9d714d (diff)
lint project
-rw-r--r--src/app/layout.tsx44
-rw-r--r--src/app/page.tsx60
-rw-r--r--src/components/CompactTable/CompactTable.tsx65
-rw-r--r--src/components/DataChart/DataChart.tsx130
-rw-r--r--src/components/Divider/Divider.tsx18
-rw-r--r--src/components/Footer/Footer.tsx48
-rw-r--r--src/components/SubscriberTable/SubscriberTable.tsx123
-rw-r--r--src/components/SubscriberTable/SubscriberTableRow.tsx70
-rw-r--r--src/components/TitleBar/TitleBar.tsx71
-rw-r--r--src/components/channel-card.tsx131
-rw-r--r--src/components/ui/avatar.tsx76
-rw-r--r--src/components/ui/badge.tsx54
-rw-r--r--src/components/ui/card.tsx125
-rw-r--r--src/lib/utils.ts6
-rw-r--r--src/pages/stats/[slug].tsx293
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;
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage