From f4d5527cb5bf96e12d619901caf27bfe019ef34e Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Wed, 12 Jun 2024 20:26:30 -0700
Subject: [PATCH] assortment of UI improvements, add stats to menu
---
.../src/components/about/ContactList.tsx | 1 +
.../src/components/about/Description.tsx | 5 +-
.../react/src/components/about/EmailForm.tsx | 9 ++-
packages/react/src/components/about/Stats.tsx | 79 +++++++++++++++++++
.../react/src/components/player/QueueList.tsx | 1 -
.../react/src/components/sidebar/sidebar.tsx | 4 +-
packages/react/src/hooks/useFrame.ts | 2 +-
packages/react/src/routes/about/faq.tsx | 43 +++++++---
packages/react/src/routes/about/general.tsx | 71 ++++++++++++++++-
.../react/src/shadcn/ui/button.variants.ts | 4 +-
packages/react/src/shadcn/ui/card.tsx | 69 ++++++++++++++++
11 files changed, 267 insertions(+), 21 deletions(-)
create mode 100644 packages/react/src/components/about/Stats.tsx
create mode 100644 packages/react/src/shadcn/ui/card.tsx
diff --git a/packages/react/src/components/about/ContactList.tsx b/packages/react/src/components/about/ContactList.tsx
index 8d8183638..5e4ddaf02 100644
--- a/packages/react/src/components/about/ContactList.tsx
+++ b/packages/react/src/components/about/ContactList.tsx
@@ -35,6 +35,7 @@ export function ContactList() {
+
{/* {t("about.contact.discord")}
,
) {
return (
-
+
);
}
diff --git a/packages/react/src/components/about/EmailForm.tsx b/packages/react/src/components/about/EmailForm.tsx
index 777a4e017..a0384bc42 100644
--- a/packages/react/src/components/about/EmailForm.tsx
+++ b/packages/react/src/components/about/EmailForm.tsx
@@ -21,7 +21,13 @@ const formSchema = z.object({
.string()
.min(1, { message: "Email is required" })
.email({ message: "Email is invalid" }),
- message: z.string().min(80, { message: "Message is required" }),
+ message: z
+ .string()
+ .min(1, { message: "Message is required" })
+ .min(20, {
+ message: "Message must be at least 20 characters",
+ })
+ .max(500, { message: "Message must be less than 500 characters" }),
});
export function AboutFaqEmailForm() {
@@ -90,6 +96,7 @@ export function AboutFaqEmailForm() {
)}
/>
diff --git a/packages/react/src/components/about/Stats.tsx b/packages/react/src/components/about/Stats.tsx
new file mode 100644
index 000000000..15a81182c
--- /dev/null
+++ b/packages/react/src/components/about/Stats.tsx
@@ -0,0 +1,79 @@
+/**
+ * v0 by Vercel.
+ * @see https://v0.dev/t/KfbKQpc7Uhn
+ * Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app
+ */
+"use client";
+
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/shadcn/ui/card";
+import { useState, useEffect } from "react";
+
+interface StatBlockProps {
+ title: string;
+ amount: number;
+ change?: number;
+ duration: number;
+ timeText?: string; // e.g. "last week"
+}
+
+/**
+ * Renders a statistic component with an animated display of the amount.
+ *
+ * @param {StatBlockProps} props - The props for the component.
+ * @param {string} props.title - The title of the statistic.
+ * @param {number} props.amount - The initial amount to display.
+ * @param {number} props.change - The change in the amount.
+ * @param {number} props.duration - The duration of the animation in milliseconds.
+ * @param {string} props.timeText - The text to display after the amount.
+ * @return {JSX.Element} The rendered statistic component.
+ */
+export default function StatComponent({
+ title,
+ amount,
+ change,
+ duration,
+ timeText,
+}: StatBlockProps) {
+ const [displayAmount, setDisplayAmount] = useState(0);
+
+ useEffect(() => {
+ const startTime = performance.now();
+ const animationLoop = (currentTime: number) => {
+ const elapsedTime = currentTime - startTime;
+ const progress = Math.min(elapsedTime / duration, 1);
+ const currentAmount = Math.floor(progress * amount);
+ setDisplayAmount(currentAmount);
+ if (progress < 1) {
+ requestAnimationFrame(animationLoop);
+ }
+ };
+ requestAnimationFrame(animationLoop);
+ }, [amount, duration]);
+
+ return (
+
+
+ {title}
+
+ {displayAmount.toLocaleString()}
+
+
+
+ {change && (
+ 0 ? "text-green-10" : "text-red-10"}`}
+ >
+ {change > 0 ? `+${change}` : `${change}`}
+ {timeText}
+
+ )}
+
+
+ );
+}
diff --git a/packages/react/src/components/player/QueueList.tsx b/packages/react/src/components/player/QueueList.tsx
index 67047d385..e303d456c 100644
--- a/packages/react/src/components/player/QueueList.tsx
+++ b/packages/react/src/components/player/QueueList.tsx
@@ -10,7 +10,6 @@ import {
} from "@/shadcn/ui/collapsible";
import { useMemo, useState } from "react";
import NewPlaylistDialog from "../playlist/NewPlaylistDialog";
-import { cn } from "@/lib/utils";
import { WATCH_PAGE_DROPDOWN_BUTTON_STYLE } from "@/shadcn/ui/button.variants";
export function QueueList({ currentId }: { currentId?: string }) {
diff --git a/packages/react/src/components/sidebar/sidebar.tsx b/packages/react/src/components/sidebar/sidebar.tsx
index af90d1131..8144f6a6b 100644
--- a/packages/react/src/components/sidebar/sidebar.tsx
+++ b/packages/react/src/components/sidebar/sidebar.tsx
@@ -193,10 +193,10 @@ function SidebarItem({
className={cn(
"w-full justify-start",
className,
- { "text-base-12 font-semibold": isHere },
+ { "text-base-12 font-semibold tracking-tight": isHere },
{ "font-base-11 font-light": !isHere },
)}
- variant={isHere ? "default" : "ghost"}
+ variant={isHere ? "primary" : "ghost"}
onClick={isMobile ? onClose : undefined}
>
diff --git a/packages/react/src/hooks/useFrame.ts b/packages/react/src/hooks/useFrame.ts
index a697c75cd..4c44055fa 100644
--- a/packages/react/src/hooks/useFrame.ts
+++ b/packages/react/src/hooks/useFrame.ts
@@ -1,7 +1,7 @@
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
-const MobileSizeBreak = 768;
+const MobileSizeBreak = 868;
const FooterSizeBreak = 500;
export const sidebarPrefOpenAtom = atomWithStorage(
diff --git a/packages/react/src/routes/about/faq.tsx b/packages/react/src/routes/about/faq.tsx
index 4180397e6..1adddb3a0 100644
--- a/packages/react/src/routes/about/faq.tsx
+++ b/packages/react/src/routes/about/faq.tsx
@@ -10,6 +10,10 @@ import {
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
+function FaqQuestion({ children }: { children: React.ReactNode }) {
+ return {children}
;
+}
+
export function AboutFaq() {
const { t } = useTranslation();
@@ -17,7 +21,9 @@ export function AboutFaq() {
- {t("about.faq.ytchatHeader")}
+
+ {t("about.faq.ytchatHeader")}
+
{t("about.faq.ytchatContent")}
@@ -32,7 +38,9 @@ export function AboutFaq() {
- {t("about.faq.autoplayHeader")}
+
+ {t("about.faq.autoplayHeader")}
+
{t("about.faq.autoplayContent")}
@@ -44,7 +52,9 @@ export function AboutFaq() {
- {t("about.faq.mobile.title")}
+
+ {t("about.faq.mobile.title")}
+
{t("about.faq.mobile.content.summary")}
@@ -65,7 +75,7 @@ export function AboutFaq() {
- {t("about.faq.favorite.disappear.title")}
+ {t("about.faq.favorite.disappear.title")}
@@ -74,7 +84,9 @@ export function AboutFaq() {
- {t("about.faq.subber.title")}
+
+ {t("about.faq.subber.title")}
+
{t("about.faq.subber.contents.0")}
@@ -89,7 +101,9 @@ export function AboutFaq() {
- {t("about.faq.videoLinkage")}
+
+ {t("about.faq.videoLinkage")}
+
{t("about.faq.videoLinkageContent")}
@@ -97,7 +111,9 @@ export function AboutFaq() {
- {t("about.faq.quitHolodex")}
+
+ {t("about.faq.quitHolodex")}
+
{t("about.faq.quitHolodexContent")}
@@ -105,7 +121,9 @@ export function AboutFaq() {
- {t("about.faq.feedback.title")}
+
+ {t("about.faq.feedback.title")}
+
{t("about.faq.feedback.contents.0")}
@@ -113,7 +131,9 @@ export function AboutFaq() {
- {t("about.faq.support.title")}
+
+ {t("about.faq.support.title")}
+
- {t("about.gdpr")}
+
+ {t("about.gdpr")}
+
{t("about.gdprContent")}
@@ -148,6 +170,7 @@ export function AboutFaq() {
+
);
diff --git a/packages/react/src/routes/about/general.tsx b/packages/react/src/routes/about/general.tsx
index 830bd9b42..913f75f09 100644
--- a/packages/react/src/routes/about/general.tsx
+++ b/packages/react/src/routes/about/general.tsx
@@ -1,6 +1,8 @@
import { AboutDescription } from "@/components/about/Description";
import { AboutHeading } from "@/components/about/Heading";
import { QuickLink, QuickLinkProps } from "@/components/about/QuickLink";
+import StatComponent from "@/components/about/Stats";
+import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
@@ -49,11 +51,13 @@ export function AboutGeneral() {
);
return (
-
+
{t("about.general.summary.title")}
{t("about.general.summary.0")}
{t("about.general.summary.1")}
{t("about.general.summary.2")}
+ {t("component.channelInfo.stats")}
+
{t("about.quicklinks")}
{quickLinks.map((link) => (
@@ -77,3 +81,68 @@ export function AboutGeneral() {
);
}
+
+interface Metrics {
+ statistics: {
+ channelCount: {
+ vtuber?: number;
+ subber?: number;
+ };
+ monthlyChannels: {
+ vtuber?: number;
+ subber?: number;
+ };
+ totalVideos: {
+ count?: number;
+ };
+ dailyVideos: {
+ count?: number;
+ };
+ totalSongs: {
+ count?: number;
+ };
+ };
+}
+function StatsBlock() {
+ const { data: stats, isSuccess } = useQuery
({
+ queryKey: ["stats"],
+ queryFn: () => fetch("/statics/stats.json").then((res) => res.json()),
+ staleTime: 50000,
+ });
+
+ if (!isSuccess || !stats) {
+ // return a loading state using Shadcn
+ return Loading...
;
+ }
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/packages/react/src/shadcn/ui/button.variants.ts b/packages/react/src/shadcn/ui/button.variants.ts
index a686007c5..15458ad16 100644
--- a/packages/react/src/shadcn/ui/button.variants.ts
+++ b/packages/react/src/shadcn/ui/button.variants.ts
@@ -9,8 +9,10 @@ export const buttonVariants = cva(
"bg-base-3 text-base-12 hover:bg-base-4 focus-visible:ring-primary-7 active:bg-primaryA-7",
outline:
"border border-primary-7 bg-transparent hover:border-primaryA-8 hover:bg-primaryA-5 focus-visible:ring-primary-7",
+ primary:
+ "bg-primary-9 text-base-12 hover:bg-primaryA-4 focus-visible:ring-primary-7",
secondary:
- "bg-secondary-9 text-base-12 hover:bg-secondaryA-4 focus-visible:ring-secondary-7 ",
+ "bg-secondary-9 text-base-12 hover:bg-secondaryA-4 focus-visible:ring-secondary-7",
ghost:
"hover:bg-base-4 hover:text-base-12 focus-visible:ring-primary-7 active:bg-primaryA-7",
"ghost-yt":
diff --git a/packages/react/src/shadcn/ui/card.tsx b/packages/react/src/shadcn/ui/card.tsx
new file mode 100644
index 000000000..522b7c91a
--- /dev/null
+++ b/packages/react/src/shadcn/ui/card.tsx
@@ -0,0 +1,69 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }