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 }