Repository: realChakrawarti/ingest
Pull request #178: Better player UX and app config for hardcoded values
Author: login: realChakrawarti
Created: 2025-05-05T14:35:49Z | Merged: 2025-05-06T17:48:23Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a centralized app configuration, enabling dynamic branding, URLs, and metadata across the application.
  - Added a custom hook for advanced YouTube video playback tracking and history management.

- **Refactor**
  - Replaced hardcoded branding, URLs, and repository links with dynamic values from the shared configuration throughout headers, footers, metadata, sharing, and FAQ sections.
  - Modularized YouTube player logic for improved playback coordination and state restoration.
  - Updated layout and page metadata titles to dynamically reflect current market name from shared configuration.

- **Style**
  - Updated interface to consistently reflect configurable market and organization names.

- **Documentation**
  - Enhanced FAQ content to dynamically display current branding and repository information.

- **Tests**
  - Broadened accepted URL formats for external links.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/src/app/(views)/(public-archive)/a/[archiveId]/page.tsx b/src/app/(views)/(public-archive)/a/[archiveId]/page.tsx
index caf22723..0ff85493 100644
--- a/src/app/(views)/(public-archive)/a/[archiveId]/page.tsx
+++ b/src/app/(views)/(public-archive)/a/[archiveId]/page.tsx
@@ -1,5 +1,6 @@
 import { Metadata } from "next/types";
 
+import appConfig from "~/shared/app-config";
 import fetchApi from "~/shared/lib/api/fetch";
 import PublicArchive from "~/views/public-archive";
 
@@ -16,13 +17,13 @@ export async function generateMetadata({
   const archiveData = result.data;
 
   return {
-    title: `${archiveData?.title} | YTCatalog`,
+    title: `${archiveData?.title} | ${appConfig.marketName}`,
     openGraph: {
       description: archiveData?.description,
-      siteName: "YTCatalog",
+      siteName: `${appConfig.marketName}`,
       title: archiveData?.title,
       type: "website",
-      url: `https://ytcatalog.707x.in/a/${archiveId}`,
+      url: `${appConfig.url}/a/${archiveId}`,
     },
   };
 }
diff --git a/src/app/(views)/(public-catalog)/c/[catalogId]/page.tsx b/src/app/(views)/(public-catalog)/c/[catalogId]/page.tsx
index 91825299..d6b4cf3f 100644
--- a/src/app/(views)/(public-catalog)/c/[catalogId]/page.tsx
+++ b/src/app/(views)/(public-catalog)/c/[catalogId]/page.tsx
@@ -1,6 +1,7 @@
 /* eslint-disable @next/next/no-img-element */
 import { Metadata } from "next/types";
 
+import appConfig from "~/shared/app-config";
 import fetchApi from "~/shared/lib/api/fetch";
 import PubliCatalog from "~/views/public-catalog";
 
@@ -20,13 +21,13 @@ export async function generateMetadata({
   const catalogData = result.data;
 
   return {
-    title: `${catalogData?.title} | YTCatalog`,
+    title: `${catalogData?.title} | ${appConfig.marketName}`,
     openGraph: {
       description: catalogData?.description,
-      siteName: "YTCatalog",
+      siteName: `${appConfig.marketName}`,
       title: catalogData?.title,
       type: "website",
-      url: `https://ytcatalog.707x.in/${catalogId}`,
+      url: `${appConfig.url}/${catalogId}`,
     },
   };
 }
diff --git a/src/app/(views)/archives/[archiveId]/edit/layout.tsx b/src/app/(views)/archives/[archiveId]/edit/layout.tsx
index 34812f50..b52a4896 100644
--- a/src/app/(views)/archives/[archiveId]/edit/layout.tsx
+++ b/src/app/(views)/archives/[archiveId]/edit/layout.tsx
@@ -1,7 +1,9 @@
 import { Metadata } from "next";
 
+import appConfig from "~/shared/app-config";
+
 export const metadata: Metadata = {
-  title: "Edit Archive | YTCatalog",
+  title: `Edit Archive | ${appConfig.marketName}`,
 };
 
 export default function DashboardLayout({
diff --git a/src/app/(views)/catalogs/[catalogId]/edit/layout.tsx b/src/app/(views)/catalogs/[catalogId]/edit/layout.tsx
index edcaa78d..608206e7 100644
--- a/src/app/(views)/catalogs/[catalogId]/edit/layout.tsx
+++ b/src/app/(views)/catalogs/[catalogId]/edit/layout.tsx
@@ -1,7 +1,9 @@
 import { Metadata } from "next";
 
+import appConfig from "~/shared/app-config";
+
 export const metadata: Metadata = {
-  title: "Edit Catalog | YTCatalog",
+  title: `Edit Catalog | ${appConfig.marketName}`,
 };
 
 export default function DashboardLayout({
diff --git a/src/app/(views)/dashboard/layout.tsx b/src/app/(views)/dashboard/layout.tsx
index 58d65d34..0aba103d 100644
--- a/src/app/(views)/dashboard/layout.tsx
+++ b/src/app/(views)/dashboard/layout.tsx
@@ -1,7 +1,9 @@
 import { Metadata } from "next";
 
+import appConfig from "~/shared/app-config";
+
 export const metadata: Metadata = {
-  title: "Dashboard | YTCatalog",
+  title: `Dashboard | ${appConfig.marketName}`,
 };
 
 export default function DashboardLayout({
diff --git a/src/app/(views)/explore/archives/layout.tsx b/src/app/(views)/explore/archives/layout.tsx
index c8cb9b7f..1852d8fe 100644
--- a/src/app/(views)/explore/archives/layout.tsx
+++ b/src/app/(views)/explore/archives/layout.tsx
@@ -3,8 +3,10 @@ import "slick-carousel/slick/slick-theme.css";
 
 import { Metadata } from "next";
 
+import appConfig from "~/shared/app-config";
+
 export const metadata: Metadata = {
-  title: "Archives | YTCatalog",
+  title: `Archives | ${appConfig.marketName}`,
 };
 
 export default function ArchivesLayout({
diff --git a/src/app/(views)/explore/catalogs/layout.tsx b/src/app/(views)/explore/catalogs/layout.tsx
index caf21e36..f164a7af 100644
--- a/src/app/(views)/explore/catalogs/layout.tsx
+++ b/src/app/(views)/explore/catalogs/layout.tsx
@@ -3,8 +3,10 @@ import "slick-carousel/slick/slick-theme.css";
 
 import { Metadata } from "next";
 
+import appConfig from "~/shared/app-config";
+
 export const metadata: Metadata = {
-  title: "Catalogs | YTCatalog",
+  title: `Catalogs | ${appConfig.marketName}`,
 };
 
 export default function CatalogsLayout({
diff --git a/src/app/(views)/explore/history/layout.tsx b/src/app/(views)/explore/history/layout.tsx
index 8eca334f..055f4045 100644
--- a/src/app/(views)/explore/history/layout.tsx
+++ b/src/app/(views)/explore/history/layout.tsx
@@ -1,7 +1,9 @@
 import { Metadata } from "next";
 
+import appConfig from "~/shared/app-config";
+
 export const metadata: Metadata = {
-  title: "History | YTCatalog",
+  title: `History | ${appConfig.marketName}`,
 };
 
 export default function HistoryLayout({
diff --git a/src/app/(views)/explore/layout.tsx b/src/app/(views)/explore/layout.tsx
index f1b7cc34..75383d4d 100644
--- a/src/app/(views)/explore/layout.tsx
+++ b/src/app/(views)/explore/layout.tsx
@@ -3,11 +3,13 @@ import "slick-carousel/slick/slick-theme.css";
 
 import { Metadata } from "next";
 
+import appConfig from "~/shared/app-config";
+
 export const metadata: Metadata = {
-  title: "Explore | YTCatalog",
+  title: `Explore | ${appConfig.marketName}`,
 };
 
-export default function DashboardLayout({
+export default function ExploreLayout({
   children,
 }: {
   children: React.ReactNode;
diff --git a/src/app/(views)/explore/watch-later/layout.tsx b/src/app/(views)/explore/watch-later/layout.tsx
index 4891e987..1b163af1 100644
--- a/src/app/(views)/explore/watch-later/layout.tsx
+++ b/src/app/(views)/explore/watch-later/layout.tsx
@@ -1,7 +1,9 @@
 import { Metadata } from "next";
 
+import appConfig from "~/shared/app-config";
+
 export const metadata: Metadata = {
-  title: "Watch later | YTCatalog",
+  title: `Watch later | ${appConfig.marketName}`,
 };
 
 export default function WatchLaterLayout({
diff --git a/src/app/(views)/layout.tsx b/src/app/(views)/layout.tsx
index e8feac63..913e93c4 100644
--- a/src/app/(views)/layout.tsx
+++ b/src/app/(views)/layout.tsx
@@ -5,6 +5,7 @@ import type { Metadata } from "next";
 import { Inter } from "next/font/google";
 import Script from "next/script";
 
+import appConfig from "~/shared/app-config";
 import { Toaster } from "~/shared/ui/toaster";
 import AppSidebar from "~/widgets/app-sidebar";
 import BackgroundPattern from "~/widgets/background-pattern";
@@ -17,11 +18,11 @@ import Providers from "./context";
 const inter = Inter({ subsets: ["latin"] });
 
 export const metadata: Metadata = {
-  applicationName: "YTCatalog",
+  applicationName: `${appConfig.marketName}`,
   description:
     "Discover new channels, curate your favorite videos, and stay organized.",
   keywords: ["youtube", "catalog", "channels", "videos", "organize"],
-  title: "YTCatalog - Organize Your YouTube Universe",
+  title: `${appConfig.marketName} - Organize Your YouTube Universe`,
 };
 
 export default function RootLayout({
diff --git a/src/app/(views)/openapi/layout.tsx b/src/app/(views)/openapi/layout.tsx
index 1c86e13a..83b28bca 100644
--- a/src/app/(views)/openapi/layout.tsx
+++ b/src/app/(views)/openapi/layout.tsx
@@ -1,7 +1,9 @@
 import { Metadata } from "next";
 
+import appConfig from "~/shared/app-config";
+
 export const metadata: Metadata = {
-  title: "OpenAPI | YTCatalog",
+  title: `OpenAPI | ${appConfig.marketName}`,
 };
 
 export default function OpenApiLayout({
diff --git a/src/shared/app-config.ts b/src/shared/app-config.ts
new file mode 100644
index 00000000..949abc0b
--- /dev/null
+++ b/src/shared/app-config.ts
@@ -0,0 +1,47 @@
+import packageInfo from "../../package.json" assert { type: "json" };
+
+class AppConfig {
+  private _domain: string = "707x.in";
+  private _marketName: string = "YTCatalog";
+  private _name: string = packageInfo.name;
+  private _subDomain: string = "ytcatalog";
+  private _version: string = packageInfo.version;
+  private _organization: string = "707x Labs";
+  private _githubRepo: string = "https://github.com/realChakrawarti/yt-catalog";
+
+  get url(): string {
+    return `https://${this._subDomain}.${this._domain}`;
+  }
+
+  get domain(): string {
+    return this._domain;
+  }
+
+  get marketName(): string {
+    return this._marketName;
+  }
+
+  get name(): string {
+    return this._name;
+  }
+
+  get subDomain(): string {
+    return this._subDomain;
+  }
+
+  get version(): string {
+    return this._version;
+  }
+
+  get organization(): string {
+    return this._organization;
+  }
+
+  get githubRepo(): string {
+    return this._githubRepo;
+  }
+}
+
+const appConfig = new AppConfig();
+
+export default appConfig;
diff --git a/src/views/public-catalog/share-catalog.tsx b/src/views/public-catalog/share-catalog.tsx
index c98fc65f..01bac92e 100644
--- a/src/views/public-catalog/share-catalog.tsx
+++ b/src/views/public-catalog/share-catalog.tsx
@@ -3,6 +3,7 @@
 import { CopyIcon, ShareIcon } from "lucide-react";
 import { useMemo } from "react";
 
+import appConfig from "~/shared/app-config";
 import { toast } from "~/shared/hooks/use-toast";
 
 export default function ShareCatalog(props: any) {
@@ -10,7 +11,7 @@ export default function ShareCatalog(props: any) {
     () => ({
       text: props.catalogDescription,
       title: props.catalogTitle,
-      url: `https://ytcatalog.707x.in/c/${props.catalogId}`,
+      url: `${appConfig.url}/c/${props.catalogId}`,
     }),
     [props]
   );
diff --git a/src/widgets/faq-section.tsx b/src/widgets/faq-section.tsx
index 6496e603..cf2aef7a 100644
--- a/src/widgets/faq-section.tsx
+++ b/src/widgets/faq-section.tsx
@@ -1,9 +1,11 @@
+/* eslint-disable @stylistic/max-len */
 "use client";
 
 import Linkify from "linkify-react";
 import { ChevronDownIcon } from "lucide-react";
 import { useState } from "react";
 
+import appConfig from "~/shared/app-config";
 import { cn } from "~/shared/lib/tailwind-merge";
 
 interface FAQItem {
@@ -13,27 +15,23 @@ interface FAQItem {
 
 const faqs: FAQItem[] = [
   {
-    question: "What can I do with YTCatalog?",
-    answer:
-      "YTCatalog lets you create personalized collections of YouTube content. Instead of just subscribing to channels, you can build custom catalogs tailored to your interests. This makes it much easier to find exactly the videos you want to watch, when you want to watch them.",
+    question: `What can I do with ${appConfig.marketName}?`,
+    answer: `${appConfig.marketName} lets you create personalized collections of YouTube content. Instead of just subscribing to channels, you can build custom catalogs tailored to your interests. This makes it much easier to find exactly the videos you want to watch, when you want to watch them.`,
   },
   {
-    question: "How do I use YTCatalog to organize my YouTube content?",
-    answer:
-      "It's simple! You select the YouTube channels you like, and you can even choose specific playlists from those channels for finer-grained control. YTCatalog then automatically adds new videos to your curated catalogs, so you don't miss anything. Think of it as your own personalized TV guide for YouTube!",
+    question: `How do I use ${appConfig.marketName} to organize my YouTube content?`,
+    answer: `It's simple! You select the YouTube channels you like, and you can even choose specific playlists from those channels for finer-grained control. ${appConfig.marketName} then automatically adds new videos to your curated catalogs, so you don't miss anything. Think of it as your own personalized TV guide for YouTube!`,
   },
   {
     question: "How often are new videos added to my catalogs?",
-    answer:
-      "YTCatalog checks for new videos from your selected channels and playlists every 4 hours, so your catalogs are always up-to-date.",
+    answer: `${appConfig.marketName} checks for new videos from your selected channels and playlists every 4 hours, so your catalogs are always up-to-date.`,
   },
   {
-    question: "Can I use YTCatalog on my phone or tablet?",
-    answer:
-      "Yes, YTCatalog is designed to work seamlessly on mobile devices. Access your catalogs from your smartphone or tablet anytime, anywhere.",
+    question: `Can I use ${appConfig.marketName} on my phone or tablet?`,
+    answer: `Yes, ${appConfig.marketName} is designed to work seamlessly on mobile devices. Access your catalogs from your smartphone or tablet anytime, anywhere.`,
   },
   {
-    question: "How secure is my data on YTCatalog?",
+    question: `How secure is my data on ${appConfig.marketName}?`,
     answer:
       "We take your privacy seriously. Your viewing history and personal data are encrypted and never shared with anyone. You have full control over your data.",
   },
@@ -48,9 +46,8 @@ const faqs: FAQItem[] = [
       "Yes, you can download videos anonymously using the privacy-focused cobalt.tools website. This allows you to save videos without revealing your identity or browsing history.",
   },
   {
-    question: "Is YTCatalog open source?",
-    answer:
-      "Yes! YTCatalog is completely open source and is hosted on GitHub. You can view the code, contribute to the project, or even host your own version.  Check it out here: https://github.com/realChakrawarti/yt-catalog.",
+    question: `Is ${appConfig.marketName} open source?`,
+    answer: `Yes! ${appConfig.marketName} is completely open source and is hosted on GitHub. You can view the code, contribute to the project, or even host your own version.  Check it out here: ${appConfig.githubRepo}.`,
   },
 ];
 
diff --git a/src/widgets/footer.tsx b/src/widgets/footer.tsx
index 1a21427a..de763430 100644
--- a/src/widgets/footer.tsx
+++ b/src/widgets/footer.tsx
@@ -1,4 +1,5 @@
-import packageInfo from "../../package.json";
+import appConfig from "~/shared/app-config";
+
 import { OutLink } from "./out-link";
 
 export default function Footer() {
@@ -10,17 +11,17 @@ export default function Footer() {
             Developed by{" "}
             <OutLink
               className="cursor-pointer text-[hsl(var(--primary))] hover:text-[hsl(var(--primary))]/70"
-              href="https://707x.in"
+              href={`https://${appConfig.domain}`}
             >
-              707x Labs
+              {appConfig.organization}
             </OutLink>
           </p>
         </div>
         <OutLink
           className="cursor-pointer text-xs dark:text-gray-300 dark:hover:text-gray-100"
-          href="https://github.com/realChakrawarti/yt-catalog/blob/main/CHANGELOG.md"
+          href={`${appConfig.githubRepo}/blob/main/CHANGELOG.md`}
         >
-          <p className="tracking-wider">v{packageInfo.version}</p>
+          <p className="tracking-wider">v{appConfig.version}</p>
         </OutLink>
       </div>
     </footer>
diff --git a/src/widgets/header.tsx b/src/widgets/header.tsx
index e24ca2ee..566efd0f 100644
--- a/src/widgets/header.tsx
+++ b/src/widgets/header.tsx
@@ -3,6 +3,7 @@
 import Image from "next/image";
 import Link from "next/link";
 
+import appConfig from "~/shared/app-config";
 import { useScrollTrigger } from "~/shared/hooks/use-scroll-trigger";
 import { cn } from "~/shared/lib/tailwind-merge";
 
@@ -53,9 +54,16 @@ const Header = () => {
           </SidebarTrigger>
           <Link href="/">
             <h1 className="flex gap-1">
-              <Image src={AppIcon} alt="YTCatalog logo" className="size-7" />
-              <p className="self-end text-lg tracking-wide dark:text-white/80 dark:hover:text-white text-primary/80 hover:text-primary" aria-hidden="true">
-                YTCatalog
+              <Image
+                src={AppIcon}
+                alt={`${appConfig.marketName} logo`}
+                className="size-7"
+              />
+              <p
+                className="self-end text-lg tracking-wide dark:text-white/80 dark:hover:text-white text-primary/80 hover:text-primary"
+                aria-hidden="true"
+              >
+                {appConfig.marketName}
               </p>
             </h1>
           </Link>
diff --git a/src/widgets/item-card.tsx b/src/widgets/item-card.tsx
index 547df284..2e100713 100644
--- a/src/widgets/item-card.tsx
+++ b/src/widgets/item-card.tsx
@@ -2,6 +2,7 @@ import { Check, Clock, Copy, Trash2 } from "lucide-react";
 import Link from "next/link";
 import { ReactNode, useState } from "react";
 
+import appConfig from "~/shared/app-config";
 import { toast } from "~/shared/hooks/use-toast";
 import { cn } from "~/shared/lib/tailwind-merge";
 import { Button } from "~/shared/ui/button";
@@ -31,7 +32,7 @@ function CopyButton({ id, type }: any) {
   const copyToClipboard = () => {
     const exploreType = type === "catalog" ? "c" : "a";
     navigator.clipboard
-      .writeText(`https://ytcatalog.707x.in/${exploreType}/${id}`)
+      .writeText(`${appConfig.url}/${exploreType}/${id}`)
       .then(() => {
         setCopied(true);
         toast({ title: "Catalog link has been copied to your clipboard." });
diff --git a/src/widgets/out-link.tsx b/src/widgets/out-link.tsx
index 11adb892..8ddf1e94 100644
--- a/src/widgets/out-link.tsx
+++ b/src/widgets/out-link.tsx
@@ -1,7 +1,7 @@
 import { ComponentProps } from "react";
 
 interface OutLinkProps extends ComponentProps<"a"> {
-  href: `https://${string}`;
+  href: `https://${string}` | string;
 }
 
 export function OutLink({
diff --git a/src/widgets/youtube/player.tsx b/src/widgets/youtube/player.tsx
index 8db86e12..b7f689bc 100644
--- a/src/widgets/youtube/player.tsx
+++ b/src/widgets/youtube/player.tsx
@@ -3,94 +3,131 @@
 import { YouTubeEmbed } from "@next/third-parties/google";
 import { useEffect, useRef } from "react";
 
+import appConfig from "~/shared/app-config";
 import { indexedDB } from "~/shared/lib/api/dexie";
-import type { History, VideoData } from "~/shared/types-schema/types";
+import type { VideoData } from "~/shared/types-schema/types";
 
-const IframeParams = "rel=0&playsinline=1&origin=https://ytcatalog.707x.in";
+import { useVideoTracking } from "./use-video-tracking";
+
+const iframeParams = `rel=0&playsinline=1&origin=${appConfig.url}`;
 
 export default function YoutubePlayer(
   props: VideoData & { enableJsApi: boolean }
 ) {
   const { enableJsApi, ...video } = props;
+
+  const { videoId, title } = video;
   const containerRef = useRef<HTMLDivElement | null>(null);
-  const trackingRef = useRef<NodeJS.Timeout | null>(null);
   const playerRef = useRef<YT.Player | null>(null);
+  const firstLoad = useRef<boolean>(false);
 
-  const startTracking = () => {
-    if (trackingRef.current) return;
-
-    trackingRef.current = setInterval(async () => {
-      if (!playerRef.current) return;
-
-      const duration = playerRef.current.getDuration() || 0;
-      const currentTime = playerRef.current.getCurrentTime() || 0;
-      const percentCompleted = parseInt(
-        ((currentTime / duration) * 100).toFixed(0)
-      );
-
-      const payload: History = {
-        completed: percentCompleted,
-        duration: currentTime,
-        updatedAt: Date.now(),
-        ...video,
-      };
-
-      await indexedDB["history"].put(payload);
-    }, 2_000);
-  };
-
-  const stopTracking = () => {
-    if (trackingRef.current) {
-      clearInterval(trackingRef.current);
-      trackingRef.current = null;
-    }
-  };
+  const { stopTracking, startTracking, isPlaying } = useVideoTracking({
+    video,
+    playerRef,
+  });
+
+  function getActivePlayers() {
+    return document.querySelectorAll("iframe");
+  }
 
-  function _onStateChange(event: YT.OnStateChangeEvent) {
+  async function _onStateChange(event: YT.OnStateChangeEvent) {
+    const { target, data: playingState } = event;
     const playerState = window.YT.PlayerState;
 
-    switch (event.data) {
-      case playerState.PLAYING:
-        startTracking();
+    const allPlayers = getActivePlayers();
+
+    const filterPlayers = Array.from(allPlayers).filter(
+      (item) => item != target.getIframe()
+    );
+
+    switch (playingState) {
+      case playerState.CUED:
+        stopTracking();
+        firstLoad.current = false;
         break;
       case playerState.PAUSED:
+        stopTracking();
+        break;
       case playerState.ENDED:
         stopTracking();
         break;
+
+      case playerState.PLAYING:
+        // Stop other players
+        filterPlayers.forEach((item) => {
+          playerControl(item, "stopVideo");
+        });
+
+        // firstLoad - The video is being played after loaded previously (after paused or stopped),
+        // isPlaying - Checks whether the video is actively playing,
+        // removal re-triggers the playing state causes a loop
+        if (!firstLoad.current && !isPlaying.current) {
+          const playedVideo = await indexedDB["history"].get(videoId);
+          if (playedVideo) {
+            target.seekTo(playedVideo.duration, true);
+            isPlaying.current = true;
+          }
+        }
+        isPlaying.current = true;
+        startTracking();
+        break;
+      case playerState.BUFFERING:
+        const seekedTime = target.getCurrentTime();
+        if (seekedTime > 0) {
+          target.seekTo(seekedTime, true);
+          isPlaying.current = true;
+        }
+        break;
     }
   }
 
+  async function playerControl(
+    iframe: HTMLIFrameElement,
+    action: "playVideo" | "stopVideo" | "pauseVideo" | "destroy"
+  ) {
+    const payload = JSON.stringify({
+      args: [],
+      event: "command",
+      func: action,
+    });
+
+    iframe.contentWindow?.postMessage(payload, "*");
+  }
+
+  // first load
   async function loadIFrameElement() {
     if (!enableJsApi) {
       return;
     }
 
-    if (!playerRef.current) {
-      playerRef.current = await (
-        containerRef.current?.querySelector("lite-youtube") as any
-      )?.getYTPlayer();
+    playerRef.current = await (
+      containerRef.current?.querySelector("lite-youtube") as any
+    )?.getYTPlayer();
 
-      const playing = await indexedDB["history"].get(video.videoId);
+    const playing = await indexedDB["history"].get(videoId);
 
-      if (playing?.videoId === video.videoId) {
-        playerRef.current?.seekTo(playing.duration, true);
-      }
+    firstLoad.current = true;
 
-      // Set up player event listeners
-      playerRef.current?.addEventListener("onStateChange", _onStateChange);
+    if (playing?.videoId === videoId) {
+      playerRef.current?.seekTo(playing.duration, true);
     }
+    // Set up player event listeners
+    playerRef.current?.addEventListener("onStateChange", _onStateChange);
   }
 
   useEffect(() => {
-    return () => stopTracking();
+    return () => {
+      stopTracking();
+      playerRef.current?.removeEventListener("onStateChange", _onStateChange);
+    };
   }, []);
 
   return (
     <div ref={containerRef} onMouseDown={loadIFrameElement}>
       <YouTubeEmbed
-        params={enableJsApi ? IframeParams + "&enablejsapi=1" : IframeParams}
-        videoid={video.videoId}
-        playlabel={video.title}
+        params={enableJsApi ? iframeParams + "&enablejsapi=1" : iframeParams}
+        videoid={videoId}
+        playlabel={title}
         js-api={enableJsApi}
       />
     </div>
diff --git a/src/widgets/youtube/use-video-tracking.ts b/src/widgets/youtube/use-video-tracking.ts
new file mode 100644
index 00000000..6e356a9d
--- /dev/null
+++ b/src/widgets/youtube/use-video-tracking.ts
@@ -0,0 +1,65 @@
+import { useEffect, useRef } from "react";
+
+import { indexedDB } from "~/shared/lib/api/dexie";
+import type { History, VideoData } from "~/shared/types-schema/types";
+
+interface UseVideoTrackingProps {
+  playerRef: React.MutableRefObject<YT.Player | null>;
+  video: VideoData;
+}
+
+export function useVideoTracking({ video, playerRef }: UseVideoTrackingProps) {
+  const trackingRef = useRef<NodeJS.Timeout | null>(null);
+  const isPlaying = useRef<boolean>(false);
+
+  function getPercentCompleted(node: YT.Player) {
+    const duration = node.getDuration() || 0;
+    const currentTime = node.getCurrentTime() || 0;
+    const percentCompleted = duration
+      ? parseInt(((currentTime / duration) * 100).toFixed(2), 10)
+      : 0;
+
+    const payload: History = {
+      completed: percentCompleted,
+      duration: +currentTime.toFixed(2),
+      updatedAt: Date.now(),
+      ...video,
+    };
+
+    return payload;
+  }
+
+  const updateProgress = async (player: YT.Player) => {
+    await indexedDB["history"].put(getPercentCompleted(player));
+  };
+
+  const startTracking = () => {
+    if (trackingRef.current) return;
+
+    trackingRef.current = setInterval(async () => {
+      if (!playerRef.current) return;
+
+      await updateProgress(playerRef.current);
+    }, 2_000);
+  };
+
+  const stopTracking = () => {
+    isPlaying.current = false;
+
+    if (trackingRef.current) {
+      clearInterval(trackingRef.current);
+      trackingRef.current = null;
+    }
+  };
+
+  useEffect(() => {
+    return () => {
+      if (trackingRef.current) {
+        clearInterval(trackingRef.current);
+        trackingRef.current = null;
+      }
+    };
+  }, []);
+
+  return { isPlaying, startTracking, stopTracking };
+}


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 11**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>src/widgets/item-card.tsx (1)</summary><blockquote>

`32-40`: _🧹 Nitpick (assertive)_

**Good use of dynamically configured URL, but consider enhancing toast message.**

Using `appConfig.url` instead of a hardcoded URL improves maintainability and ensures consistent URLs throughout the application.


Consider making the toast message dynamic to match the type being copied:

```diff
-toast({ title: "Catalog link has been copied to your clipboard." });
+toast({ title: `${type.charAt(0).toUpperCase() + type.slice(1)} link has been copied to your clipboard.` });
```

This would make the message more accurate when copying archive links.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 17719da931f67867588247e01d87ee8f63d9fafd and 17192a44446219cf135d19f4ee825d4e6af30ab9.

</details>

<details>
<summary>📒 Files selected for processing (21)</summary>

* `src/app/(views)/(public-archive)/a/[archiveId]/page.tsx` (2 hunks)
* `src/app/(views)/(public-catalog)/c/[catalogId]/page.tsx` (2 hunks)
* `src/app/(views)/archives/[archiveId]/edit/layout.tsx` (1 hunks)
* `src/app/(views)/catalogs/[catalogId]/edit/layout.tsx` (1 hunks)
* `src/app/(views)/dashboard/layout.tsx` (1 hunks)
* `src/app/(views)/explore/archives/layout.tsx` (1 hunks)
* `src/app/(views)/explore/catalogs/layout.tsx` (1 hunks)
* `src/app/(views)/explore/history/layout.tsx` (1 hunks)
* `src/app/(views)/explore/layout.tsx` (1 hunks)
* `src/app/(views)/explore/watch-later/layout.tsx` (1 hunks)
* `src/app/(views)/layout.tsx` (2 hunks)
* `src/app/(views)/openapi/layout.tsx` (1 hunks)
* `src/shared/app-config.ts` (1 hunks)
* `src/views/public-catalog/share-catalog.tsx` (1 hunks)
* `src/widgets/faq-section.tsx` (3 hunks)
* `src/widgets/footer.tsx` (2 hunks)
* `src/widgets/header.tsx` (2 hunks)
* `src/widgets/item-card.tsx` (2 hunks)
* `src/widgets/out-link.tsx` (1 hunks)
* `src/widgets/youtube/player.tsx` (1 hunks)
* `src/widgets/youtube/useVideoTracking.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (9)</summary>

<details>
<summary>src/app/(views)/dashboard/layout.tsx (9)</summary><blockquote>

<details>
<summary>src/app/(views)/catalogs/[catalogId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/archives/[archiveId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/archives/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/catalogs/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/history/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/watch-later/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/layout.tsx (1)</summary>

* `metadata` (20-26)

</details>
<details>
<summary>src/app/(views)/openapi/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>

</blockquote></details>
<details>
<summary>src/app/(views)/explore/watch-later/layout.tsx (8)</summary><blockquote>

<details>
<summary>src/app/(views)/catalogs/[catalogId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/archives/[archiveId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/archives/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/catalogs/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/history/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/dashboard/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/layout.tsx (1)</summary>

* `metadata` (20-26)

</details>

</blockquote></details>
<details>
<summary>src/app/(views)/explore/layout.tsx (9)</summary><blockquote>

<details>
<summary>src/app/(views)/catalogs/[catalogId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/archives/[archiveId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/archives/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/catalogs/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/history/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/dashboard/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/watch-later/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/layout.tsx (1)</summary>

* `metadata` (20-26)

</details>
<details>
<summary>src/app/(views)/openapi/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>

</blockquote></details>
<details>
<summary>src/app/(views)/explore/catalogs/layout.tsx (9)</summary><blockquote>

<details>
<summary>src/app/(views)/catalogs/[catalogId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/archives/[archiveId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/archives/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/history/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/dashboard/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/watch-later/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/layout.tsx (1)</summary>

* `metadata` (20-26)

</details>
<details>
<summary>src/app/(views)/openapi/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>

</blockquote></details>
<details>
<summary>src/app/(views)/catalogs/[catalogId]/edit/layout.tsx (6)</summary><blockquote>

<details>
<summary>src/app/(views)/archives/[archiveId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/catalogs/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/history/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/dashboard/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/layout.tsx (1)</summary>

* `metadata` (20-26)

</details>
<details>
<summary>src/app/(views)/openapi/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>

</blockquote></details>
<details>
<summary>src/app/(views)/archives/[archiveId]/edit/layout.tsx (1)</summary><blockquote>

<details>
<summary>src/app/(views)/layout.tsx (1)</summary>

* `metadata` (20-26)

</details>

</blockquote></details>
<details>
<summary>src/app/(views)/openapi/layout.tsx (9)</summary><blockquote>

<details>
<summary>src/app/(views)/catalogs/[catalogId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/archives/[archiveId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/archives/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/catalogs/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/history/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/dashboard/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/watch-later/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/layout.tsx (1)</summary>

* `metadata` (20-26)

</details>

</blockquote></details>
<details>
<summary>src/widgets/footer.tsx (1)</summary><blockquote>

<details>
<summary>src/widgets/out-link.tsx (1)</summary>

* `OutLink` (7-18)

</details>

</blockquote></details>
<details>
<summary>src/widgets/youtube/useVideoTracking.ts (2)</summary><blockquote>

<details>
<summary>src/shared/types-schema/types.ts (2)</summary>

* `VideoData` (60-60)
* `History` (43-47)

</details>
<details>
<summary>src/shared/lib/api/dexie.ts (1)</summary>

* `indexedDB` (20-20)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>src/shared/app-config.ts</summary>

[error] 4-4: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 5-5: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 7-7: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 9-9: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 10-10: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

</details>
<details>
<summary>src/widgets/youtube/player.tsx</summary>

[error] 120-120: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.


(lint/style/useTemplate)

</details>
<details>
<summary>src/widgets/youtube/useVideoTracking.ts</summary>

[error] 18-18: Use Number.parseInt instead of the equivalent global.

ES2015 moved some globals into the Number namespace for consistency.
Safe fix: Use Number.parseInt instead.


(lint/style/useNumberNamespace)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (36)</summary><blockquote>

<details>
<summary>src/shared/app-config.ts (1)</summary>

`3-10`: _🧹 Nitpick (assertive)_

**Remove trivially-inferred type annotations**

All five private fields already have a literal initializer, so the explicit `: string` type annotation is redundant and is flagged by Biome (`noInferrableTypes`).  
Removing the annotations keeps the file terse without losing type-safety.

```diff
-  private _domain: string = "707x.in";
-  private _marketName: string = "YTCatalog";
-  private _name: string = packageInfo.name;
-  private _subDomain: string = "ytcatalog";
-  private _version: string = packageInfo.version;
-  private _organization: string = "707x Labs";
-  private _githubRepo: string = "https://github.com/realChakrawarti/yt-catalog";
+  private _domain = "707x.in";
+  private _marketName = "YTCatalog";
+  private _name = packageInfo.name;
+  private _subDomain = "ytcatalog";
+  private _version = packageInfo.version;
+  private _organization = "707x Labs";
+  private _githubRepo = "https://github.com/realChakrawarti/yt-catalog";
```




> Likely an incorrect or invalid review comment.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 4-4: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 5-5: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 7-7: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 9-9: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 10-10: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

</details>

</details>

</details>
<details>
<summary>src/app/(views)/dashboard/layout.tsx (1)</summary>

`3-6`: **LGTM!**

Good job replacing the hardcoded title with the configurable `appConfig.marketName`. This change is consistent with the pattern used across other layout files and improves maintainability.

</details>
<details>
<summary>src/app/(views)/(public-archive)/a/[archiveId]/page.tsx (4)</summary>

`3-3`: **LGTM!**

Good addition of the appConfig import to replace hardcoded values.

---

`20-20`: **LGTM!**

Properly replaced the hardcoded title with the configurable market name from appConfig.

---

`23-23`: **LGTM!**

Good update to use the configurable market name for the openGraph siteName property.

---

`26-26`: **LGTM!**

Well implemented dynamic URL for the openGraph.url property using appConfig.url, which will ensure correct absolute URLs for social sharing.

</details>
<details>
<summary>src/app/(views)/explore/layout.tsx (1)</summary>

`6-9`: **LGTM!**

Good replacement of the hardcoded title with the configurable `appConfig.marketName`, consistent with other layout files.

</details>
<details>
<summary>src/widgets/header.tsx (3)</summary>

`6-6`: **Good addition of centralized configuration import.**

Adding the `appConfig` import allows for consistent branding and configuration management across the application.

---

`57-61`: **Excellent replacement of hardcoded alt text with dynamic value.**

Using `${appConfig.marketName} logo` for the alt text improves accessibility and ensures consistent branding.

---

`62-67`: **Good replacement of hardcoded brand name with configurable value.**

Replacing the hardcoded brand name with `{appConfig.marketName}` makes the application more maintainable and allows for easier rebranding if needed.

</details>
<details>
<summary>src/app/(views)/archives/[archiveId]/edit/layout.tsx (2)</summary>

`3-4`: **Good addition of app configuration import.**

Adding the import for the centralized configuration object is a good practice for maintaining consistent branding.

---

`6-6`: **Well-implemented dynamic metadata title.**

Replacing the hardcoded title with a template string using `appConfig.marketName` aligns with the overall goal of centralizing configuration values.

</details>
<details>
<summary>src/app/(views)/catalogs/[catalogId]/edit/layout.tsx (2)</summary>

`3-4`: **Good addition of app configuration import.**

Adding the import for the centralized configuration object is a good practice for maintaining consistent branding.

---

`6-6`: **Well-implemented dynamic metadata title.**

Replacing the hardcoded title with a template string using `appConfig.marketName` aligns with the overall goal of centralizing configuration values.

</details>
<details>
<summary>src/app/(views)/explore/history/layout.tsx (2)</summary>

`3-4`: **Good addition of app configuration import.**

Adding the import for the centralized configuration object is a good practice for maintaining consistent branding.

---

`6-6`: **Well-implemented dynamic metadata title.**

Replacing the hardcoded title with a template string using `appConfig.marketName` aligns with the overall goal of centralizing configuration values.

</details>
<details>
<summary>src/app/(views)/explore/archives/layout.tsx (1)</summary>

`6-6`: **Good use of centralized configuration**

The change replaces hardcoded branding with values from the appConfig. This makes the application more maintainable as brand names can be changed in a single location.



Also applies to: 9-9

</details>
<details>
<summary>src/views/public-catalog/share-catalog.tsx (1)</summary>

`6-6`: **Properly implemented configurable URL**

Replacing the hardcoded URL with the value from appConfig is a good practice. This ensures consistency across the application and simplifies domain changes.



Also applies to: 14-14

</details>
<details>
<summary>src/app/(views)/(public-catalog)/c/[catalogId]/page.tsx (1)</summary>

`4-4`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify the URL path structure in the OpenGraph URL**

The configurable URLs are a good improvement, but there appears to be an inconsistency in the path structure:
- In this file: `${appConfig.url}/${catalogId}`
- In share-catalog.tsx: `${appConfig.url}/c/${catalogId}`

These should be consistent to avoid broken links.




Also applies to: 24-24, 27-27, 30-30

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for other URL path patterns used with catalogId
rg "appConfig\.url.*catalogId" --type=tsx
```

Length of output: 71

---

🏁 Script executed:

```shell
#!/bin/bash
# Inspect page.tsx in the public catalog route for URL patterns
fd page.tsx -t f -x rg 'appConfig\.url.*catalogId' {} -n -C 3

# Inspect share-catalog.tsx for the same pattern
fd share-catalog.tsx -t f -x rg 'appConfig\.url.*catalogId' {} -n -C 3
```

Length of output: 500

---



</details>

**Ensure consistent OpenGraph URL path**

The public-catalog page is missing the `/c` segment that the share-catalog component uses. Align both to avoid broken links:

• **src/app/(views)/(public-catalog)/c/[catalogId]/page.tsx** (line 30)  
  Change:
  ```diff
  - url: `${appConfig.url}/${catalogId}`,
  + url: `${appConfig.url}/c/${catalogId}`,
  ```

• **src/components/share-catalog.tsx** (line 14)  
  Already uses:
  ```js
  url: `${appConfig.url}/c/${props.catalogId}`,
  ```

[fіx_required]

</details>
<details>
<summary>src/app/(views)/explore/catalogs/layout.tsx (1)</summary>

`6-9`: **Good implementation of centralized configuration.**

The change replaces hardcoded market name with a configurable value from `appConfig.marketName`. This is consistent with similar changes in other layout files and improves maintainability.

</details>
<details>
<summary>src/app/(views)/explore/watch-later/layout.tsx (1)</summary>

`3-6`: **Clean implementation of dynamic title configuration.**

Replacing the hardcoded title with a template string using `appConfig.marketName` aligns with the centralization pattern implemented across other layout files, making the application branding more maintainable.

</details>
<details>
<summary>src/app/(views)/layout.tsx (2)</summary>

`8-8`: **Appropriate import of appConfig for centralized configuration.**

The import is correctly placed and will be used to replace hardcoded values in the metadata.

---

`21-25`: **Good implementation of dynamic metadata in root layout.**

Using `appConfig.marketName` for both the applicationName and title fields in the root layout's metadata ensures consistent branding throughout the application and improves maintainability by centralizing configuration values.

</details>
<details>
<summary>src/widgets/item-card.tsx (1)</summary>

`5-5`: **Appropriate import of appConfig for URL configuration.**

The import is correctly placed and will be used to replace the hardcoded URL in the copyToClipboard function.

</details>
<details>
<summary>src/widgets/faq-section.tsx (7)</summary>

`8-8`: **Good addition of appConfig import**

Adding the import for the centralized configuration is appropriate and aligns with the PR objective of replacing hardcoded values.

---

`18-20`: **Well-implemented dynamic branding**

Good job replacing the hardcoded brand name with the dynamic `appConfig.marketName` in both the question and answer. This improves maintainability by centralizing the app's branding.

---

`22-24`: **Appropriate replacement of hardcoded values**

The hardcoded brand name has been properly replaced with `appConfig.marketName` in this FAQ item, making it consistent throughout the application.

---

`26-28`: **Consistent branding implementation**

The dynamic reference to `appConfig.marketName` maintains consistency in the app's branding across all FAQ content.

---

`30-32`: **Good continuation of dynamic branding pattern**

The consistent pattern of using `appConfig.marketName` is maintained, which will help if the brand name ever needs to change in the future.

---

`34-34`: **Proper configuration reference**

The question correctly uses the dynamic `appConfig.marketName` reference.

---

`49-51`: **Well-implemented dynamic URL and branding**

Good job replacing both the brand name and GitHub repository URL with their respective `appConfig` properties. This makes both the branding and links configurable and consistent.

</details>
<details>
<summary>src/widgets/footer.tsx (5)</summary>

`1-2`: **Good implementation of appConfig import**

Correctly imported the centralized configuration and removed the direct import of package.json, aligning with the PR objective of using a shared configuration source.

---

`14-14`: **Proper use of dynamic domain configuration**

Good job replacing the hardcoded domain with `appConfig.domain`. The template literal is correctly formatted.

---

`16-16`: **Appropriate use of organization name from config**

The organization name is now properly sourced from `appConfig.organization` instead of being hardcoded, improving maintainability.

---

`22-22`: **Well-implemented dynamic GitHub repository URL**

The changelog link now correctly uses the GitHub repository URL from the configuration, maintaining consistency with other components that reference the repository.

---

`24-24`: **Good implementation of dynamic version display**

The version number is now properly sourced from `appConfig.version` instead of directly from package.json, providing a consistent approach to version display throughout the app.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T14:42:04Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>♻️ Duplicate comments (5)</summary><blockquote>

<details>
<summary>src/shared/app-config.ts (1)</summary><blockquote>

`45-47`: _🛠️ Refactor suggestion_

**Expose configurability through constructor or environment variables.**

The singleton is hard-wired to production values. Staging/test environments will have to patch the source code to change `domain`, `subDomain`, etc. Consider accepting an optional constructor argument or reading from `process.env` so that values can be overridden without code changes.


```diff
- class AppConfig {
-   private _domain = "707x.in";
-   private _marketName = "YTCatalog";
-   private _name = packageInfo.name;
-   private _subDomain = "ytcatalog";
-   private _version = packageInfo.version;
-   private _organization = "707x Labs";
-   private _githubRepo = "https://github.com/realChakrawarti/yt-catalog";
+ class AppConfig {
+   private _domain: string;
+   private _marketName: string;
+   private _name: string;
+   private _subDomain: string;
+   private _version: string;
+   private _organization: string;
+   private _githubRepo: string;
+   
+   constructor(config?: Partial<{
+     domain: string;
+     marketName: string;
+     name: string;
+     subDomain: string;
+     version: string;
+     organization: string;
+     githubRepo: string;
+   }>) {
+     this._domain = config?.domain ?? process.env.NEXT_PUBLIC_DOMAIN ?? "707x.in";
+     this._marketName = config?.marketName ?? process.env.NEXT_PUBLIC_MARKET_NAME ?? "YTCatalog";
+     this._name = config?.name ?? packageInfo.name;
+     this._subDomain = config?.subDomain ?? process.env.NEXT_PUBLIC_SUBDOMAIN ?? "ytcatalog";
+     this._version = config?.version ?? packageInfo.version;
+     this._organization = config?.organization ?? process.env.NEXT_PUBLIC_ORGANIZATION ?? "707x Labs";
+     this._githubRepo = config?.githubRepo ?? process.env.NEXT_PUBLIC_GITHUB_REPO ?? "https://github.com/realChakrawarti/yt-catalog";
+   }

// Then update the instantiation:
- const appConfig = new AppConfig();
+ const appConfig = new AppConfig();
```

</blockquote></details>
<details>
<summary>src/widgets/youtube/player.tsx (4)</summary><blockquote>

`134-136`: _🧹 Nitpick (assertive)_

**Biome repeat – prefer template literal over string concatenation**

The style linter still complains here:

```diff
-params={enableJsApi ? iframeParams + "&enablejsapi=1" : iframeParams}
+params={`${iframeParams}${enableJsApi ? "&enablejsapi=1" : ""}`}
```

Keeps the codebase consistent and quiets Biome.

---

`30-32`: _🛠️ Refactor suggestion_

**Selector still targets every iframe – risk of collateral `postMessage`s**  
Same concern raised earlier: non-YouTube iframes (analytics, ads, surveys…) will receive YT control messages.  
Please scope the selector, e.g.:

```diff
-return document.querySelectorAll("iframe");
+return document.querySelectorAll('iframe[src*="youtube.com"], iframe[src*="youtu.be"]');
```

---

`35-78`: _⚠️ Potential issue_

**Event-listener leak persisted (same handler name, new reference each render)**

`_onStateChange` is recreated on every render but added only during the first
`loadIFrameElement` call. When the component unmounts, the cleanup uses **a
different function reference**, so the original listener stays attached and the
player never gets garbage-collected.

Wrap the handler in `useCallback` (or stash it in `useRef`) and add it only once:

```diff
-async function _onStateChange(event: YT.OnStateChangeEvent) { ... }
+const _onStateChange = useCallback(async (event: YT.OnStateChangeEvent) => {
+  ...
+}, [stopTracking, videoId]);   // stable reference
```

Then guard `addEventListener` so it runs just once.

Memory leaks aside, this also prevents duplicated DB reads & writes when the
component rerenders.

---

`82-93`: _⚠️ Potential issue_

**Unrestricted `postMessage` target allows spoofed commands**

`playerControl` still sends messages with `targetOrigin = "*"`; any embedded
attacker iframe can spoof YT events or intercept commands. Use the iframe’s own
origin instead:

```diff
-iframe.contentWindow?.postMessage(payload, "*");
+const { origin } = new URL(iframe.src);
+iframe.contentWindow?.postMessage(payload, origin);
```

Hardening this call closes an unnecessary XSS surface.  
  [security]

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 17192a44446219cf135d19f4ee825d4e6af30ab9 and 35cf1e21229522b4d4501808dd9389f6c09ee88f.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `src/app/(views)/explore/layout.tsx` (1 hunks)
* `src/app/(views)/openapi/layout.tsx` (1 hunks)
* `src/shared/app-config.ts` (1 hunks)
* `src/widgets/youtube/player.tsx` (1 hunks)
* `src/widgets/youtube/use-video-tracking.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>src/app/(views)/openapi/layout.tsx (9)</summary><blockquote>

<details>
<summary>src/app/(views)/explore/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/catalogs/[catalogId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/archives/[archiveId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/archives/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/dashboard/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/catalogs/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/watch-later/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/history/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/layout.tsx (1)</summary>

* `metadata` (20-26)

</details>

</blockquote></details>
<details>
<summary>src/widgets/youtube/use-video-tracking.ts (2)</summary><blockquote>

<details>
<summary>src/shared/types-schema/types.ts (2)</summary>

* `VideoData` (60-60)
* `History` (43-47)

</details>
<details>
<summary>src/shared/lib/api/dexie.ts (1)</summary>

* `indexedDB` (20-20)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>src/widgets/youtube/player.tsx</summary>

[error] 120-120: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.


(lint/style/useTemplate)

</details>
<details>
<summary>src/widgets/youtube/use-video-tracking.ts</summary>

[error] 19-19: Use Number.parseInt instead of the equivalent global.

ES2015 moved some globals into the Number namespace for consistency.
Safe fix: Use Number.parseInt instead.


(lint/style/useNumberNamespace)

</details>
<details>
<summary>src/shared/app-config.ts</summary>

[error] 4-4: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 5-5: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 7-7: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 9-9: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 10-10: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>src/shared/app-config.ts (2)</summary>

`1-1`: **Import assertion is a good practice for JSON imports.**

Using the `assert { type: "json" }` syntax for JSON imports makes the intent explicit and helps maintain forward compatibility as JavaScript module systems evolve.

---

`3-43`: **Well-structured encapsulation pattern with private fields and public getters.**

The `AppConfig` class is well-organized with proper encapsulation using private fields (`_domain`, `_marketName`, etc.) and public getters, making the configuration values read-only and preventing unintended modifications.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 4-4: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 5-5: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 7-7: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 9-9: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 10-10: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

</details>

</details>

</details>
<details>
<summary>src/app/(views)/openapi/layout.tsx (1)</summary>

`3-6`: **Good usage of centralized app configuration.**

Importing and using `appConfig.marketName` for the title ensures consistent branding across the application and aligns with the pattern used in other layouts.

</details>
<details>
<summary>src/app/(views)/explore/layout.tsx (2)</summary>

`6-10`: **Good usage of centralized app configuration.**

Using `appConfig.marketName` for the title ensures consistent branding across the application.

---

`12-12`: **Function name now properly matches file purpose.**

Renaming from `DashboardLayout` to `ExploreLayout` correctly aligns the component name with its purpose and location.

</details>
<details>
<summary>src/widgets/youtube/use-video-tracking.ts (1)</summary>

`11-52`: **Well-structured hook for video tracking.**

The `useVideoTracking` hook effectively encapsulates video tracking logic, making it reusable across components that need YouTube video progress tracking.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 19-19: Use Number.parseInt instead of the equivalent global.

ES2015 moved some globals into the Number namespace for consistency.
Safe fix: Use Number.parseInt instead.


(lint/style/useNumberNamespace)

</details>

</details>

</details>
<details>
<summary>src/widgets/youtube/player.tsx (1)</summary>

`6-12`: <details>
<summary>❌ Incorrect review comment</summary>

**Sanitise `origin` to prevent malformed iframe parameter**

`appConfig.url` is interpolated directly into the `iframeParams` string.  
If the value ever contains a trailing slash, path, or query component (e.g. `"https://foo.com/"` or `"https://foo.com?stage=dev"`), the resulting param will be rejected by YouTube and the embed will silently fail.

```diff
-const iframeParams = `rel=0&playsinline=1&origin=${appConfig.url}`;
+const origin      = new URL(appConfig.url).origin; // strips path / query
+const iframeParams = `rel=0&playsinline=1&origin=${origin}`;
```

A tiny guard now, a mysterious “video unavailable” headache averted later.

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate and inspect the appConfig definition to see how `url` is set
echo "Searching for app-config files..."
fd app-config -t f
echo

for f in $(fd app-config -t f); do
  echo "=== Inspecting $f ==="
  sed -n '1,200p' "$f"
  echo
done
```

Length of output: 1336

---



</details>

**No sanitization needed for origin**  
`appConfig.url` is implemented as  
```ts
get url(): string {
  return `https://${this._subDomain}.${this._domain}`;
}
```  
which always produces a protocol-only origin (e.g. `https://ytcatalog.707x.in`) with no trailing slash, path, or query. The suggested `new URL(appConfig.url).origin` would be a no-op here.  

No changes required. 

> Likely an incorrect or invalid review comment.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T19:56:26Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (7)</summary><blockquote>

<details>
<summary>src/shared/app-config.ts (2)</summary><blockquote>

`4-10`: **Remove redundant type annotations.**

The type annotations on the private fields are unnecessary as TypeScript can infer them from the assigned values.


```diff
- private _domain: string = "707x.in";
- private _marketName: string = "YTCatalog";
- private _name: string = packageInfo.name;
- private _subDomain: string = "ytcatalog";
- private _version: string = packageInfo.version;
- private _organization: string = "707x Labs";
- private _githubRepo: string = "https://github.com/realChakrawarti/yt-catalog";
+ private _domain = "707x.in";
+ private _marketName = "YTCatalog";
+ private _name = packageInfo.name;
+ private _subDomain = "ytcatalog";
+ private _version = packageInfo.version;
+ private _organization = "707x Labs";
+ private _githubRepo = "https://github.com/realChakrawarti/yt-catalog";
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 4-4: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 5-5: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 7-7: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 9-9: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 10-10: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

</details>

</details>

---

`45-47`: **Make configuration values more flexible.**

The singleton is currently hard-coded with production values. Consider allowing configuration through constructor arguments or environment variables for different environments.


```diff
-const appConfig = new AppConfig();
+const appConfig = new AppConfig({
+  domain: process.env.NEXT_PUBLIC_DOMAIN ?? "707x.in",
+  subDomain: process.env.NEXT_PUBLIC_SUBDOMAIN ?? "ytcatalog",
+  marketName: process.env.NEXT_PUBLIC_MARKET_NAME ?? "YTCatalog",
+  organization: process.env.NEXT_PUBLIC_ORGANIZATION ?? "707x Labs",
+  githubRepo: process.env.NEXT_PUBLIC_GITHUB_REPO ?? "https://github.com/realChakrawarti/yt-catalog",
+});
```

</blockquote></details>
<details>
<summary>src/widgets/youtube/use-video-tracking.ts (2)</summary><blockquote>

`19-19`: **Use Number.parseInt instead of global parseInt.**

For consistency with modern ES standards, prefer using `Number.parseInt` instead of the global `parseInt`.


```diff
- parseInt(((currentTime / duration) * 100).toFixed(2), 10)
+ Number.parseInt(((currentTime / duration) * 100).toFixed(2), 10)
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 19-19: Use Number.parseInt instead of the equivalent global.

ES2015 moved some globals into the Number namespace for consistency.
Safe fix: Use Number.parseInt instead.


(lint/style/useNumberNamespace)

</details>

</details>

---

`38-38`: **Add error handling for IndexedDB operations.**

The IndexedDB operation should include error handling to gracefully handle potential failures.


```diff
- await indexedDB["history"].put(getPercentCompleted(playerRef.current));
+ try {
+   await indexedDB["history"].put(getPercentCompleted(playerRef.current));
+ } catch (error) {
+   console.error("Failed to update video history:", error);
+ }
```

</blockquote></details>
<details>
<summary>src/widgets/youtube/player.tsx (3)</summary><blockquote>

`33-74`: **Stable listener reference to prevent leaks (already reported earlier)**

`_onStateChange` is redeclared on every render, but the cleanup uses the *current* render’s reference.  
If the component re-renders between the `addEventListener` call and unmount, the original listener stays attached, causing memory leaks and duplicated events.

(The same issue was pointed out in a previous review; still unfixed.)

---

`76-87`: **Restrict `postMessage` target origin (security hardening) — still unresolved**

Using `"*"` allows any origin to spoof YouTube messages. Replace with the iframe’s real origin:

```diff
-    iframe.contentWindow?.postMessage(payload, "*");
+    const { origin } = new URL(iframe.src);
+    iframe.contentWindow?.postMessage(payload, origin);
```

---

`120-120`: _🧹 Nitpick (assertive)_

**Use a template literal instead of concatenation (Biome warning)**

```diff
-        params={enableJsApi ? iframeParams + "&enablejsapi=1" : iframeParams}
+        params={`${iframeParams}${enableJsApi ? "&enablejsapi=1" : ""}`}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 120-120: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.


(lint/style/useTemplate)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 35cf1e21229522b4d4501808dd9389f6c09ee88f and 9f24103507d20a2ce87f9cee1527e0f36bd40402.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `src/app/(views)/explore/layout.tsx` (1 hunks)
* `src/app/(views)/openapi/layout.tsx` (1 hunks)
* `src/shared/app-config.ts` (1 hunks)
* `src/widgets/youtube/player.tsx` (1 hunks)
* `src/widgets/youtube/use-video-tracking.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>src/app/(views)/openapi/layout.tsx (9)</summary><blockquote>

<details>
<summary>src/app/(views)/explore/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/archives/[archiveId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/catalogs/[catalogId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/dashboard/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/archives/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/catalogs/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/history/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/watch-later/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/layout.tsx (1)</summary>

* `metadata` (20-26)

</details>

</blockquote></details>
<details>
<summary>src/app/(views)/explore/layout.tsx (9)</summary><blockquote>

<details>
<summary>src/app/(views)/openapi/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/archives/[archiveId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/catalogs/[catalogId]/edit/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/dashboard/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/archives/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/catalogs/layout.tsx (1)</summary>

* `metadata` (8-10)

</details>
<details>
<summary>src/app/(views)/explore/history/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/explore/watch-later/layout.tsx (1)</summary>

* `metadata` (5-7)

</details>
<details>
<summary>src/app/(views)/layout.tsx (1)</summary>

* `metadata` (20-26)

</details>

</blockquote></details>
<details>
<summary>src/widgets/youtube/use-video-tracking.ts (2)</summary><blockquote>

<details>
<summary>src/shared/types-schema/types.ts (2)</summary>

* `VideoData` (60-60)
* `History` (43-47)

</details>
<details>
<summary>src/shared/lib/api/dexie.ts (1)</summary>

* `indexedDB` (20-20)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>src/widgets/youtube/use-video-tracking.ts</summary>

[error] 19-19: Use Number.parseInt instead of the equivalent global.

ES2015 moved some globals into the Number namespace for consistency.
Safe fix: Use Number.parseInt instead.


(lint/style/useNumberNamespace)

</details>
<details>
<summary>src/widgets/youtube/player.tsx</summary>

[error] 120-120: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.


(lint/style/useTemplate)

</details>
<details>
<summary>src/shared/app-config.ts</summary>

[error] 4-4: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 5-5: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 7-7: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 9-9: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 10-10: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>src/app/(views)/openapi/layout.tsx (1)</summary>

`3-6`: **LGTM!**

The change correctly leverages the centralized app configuration and follows the consistent pattern used in other layout files.

</details>
<details>
<summary>src/app/(views)/explore/layout.tsx (1)</summary>

`6-12`: **LGTM! Function name now matches file purpose.**

The changes correctly use the centralized app configuration and fix the function name to properly reflect the file's purpose.

</details>
<details>
<summary>src/widgets/youtube/use-video-tracking.ts (2)</summary>

`51-58`: **LGTM! Good cleanup implementation.**

The useEffect cleanup for the interval timer is well implemented, preventing memory leaks when the component unmounts.

---

`11-61`: **Well-structured hook for video tracking.**

This hook effectively encapsulates the video tracking logic in a reusable way, improving modularity and separation of concerns.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 19-19: Use Number.parseInt instead of the equivalent global.

ES2015 moved some globals into the Number namespace for consistency.
Safe fix: Use Number.parseInt instead.


(lint/style/useNumberNamespace)

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T20:06:16Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>♻️ Duplicate comments (5)</summary><blockquote>

<details>
<summary>src/widgets/youtube/use-video-tracking.ts (1)</summary><blockquote>

`32-34`: **Still missing error handling around IndexedDB writes**  

Previous review already highlighted that an un-handled Dexie error will bubble up and be swallowed by the `setInterval` promise, potentially breaking the loop silently.  
Wrapping the call in a `try / catch` and logging (or surfacing) the error protects against quota issues and corrupted DB states.

</blockquote></details>
<details>
<summary>src/widgets/youtube/player.tsx (4)</summary><blockquote>

`29-31`: **Selector is still too broad & type-unsafe**  

Earlier review suggested restricting the query to YouTube iframes and returning a typed node list to avoid sending `postMessage` to unrelated embeds. The current code still queries *all* iframes.

---

`103-116`: **`_onStateChange` reference drift leads to leaked listeners**

Because `_onStateChange` is re-created on every render, `addEventListener` and `removeEventListener` rarely receive the *same* function reference, leaving dangling listeners behind. Wrap the handler in `useCallback` or `useRef` so the identity is stable.

---

`88-95`: _🧹 Nitpick (assertive)_

**Restrict `postMessage` target origin – open security hole**

Using `"*"` allows any hostile iframe on the page to spoof YT messages. Limit the destination to the iframe’s origin:

```diff
-    iframe.contentWindow?.postMessage(payload, "*");
+    const { origin } = new URL(iframe.src);
+    iframe.contentWindow?.postMessage(payload, origin);
```

---

`128-129`: _🧹 Nitpick (assertive)_

**Prefer template literals over string concatenation**

Biome warns here; using a template literal stays consistent with the rest of the file:

```diff
-        params={enableJsApi ? iframeParams + "&enablejsapi=1" : iframeParams}
+        params={`${iframeParams}${enableJsApi ? "&enablejsapi=1" : ""}`}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 128-128: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.


(lint/style/useTemplate)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9f24103507d20a2ce87f9cee1527e0f36bd40402 and 5efe7f15b65788ea31406dfb0abc08755f9c9a2f.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `src/widgets/youtube/player.tsx` (1 hunks)
* `src/widgets/youtube/use-video-tracking.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>src/widgets/youtube/use-video-tracking.ts (2)</summary><blockquote>

<details>
<summary>src/shared/types-schema/types.ts (2)</summary>

* `VideoData` (60-60)
* `History` (43-47)

</details>
<details>
<summary>src/shared/lib/api/dexie.ts (1)</summary>

* `indexedDB` (20-20)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>src/widgets/youtube/player.tsx</summary>

[error] 75-75: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.


(lint/correctness/noSwitchDeclarations)

---

[error] 128-128: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.


(lint/style/useTemplate)

</details>
<details>
<summary>src/widgets/youtube/use-video-tracking.ts</summary>

[error] 19-19: Use Number.parseInt instead of the equivalent global.

ES2015 moved some globals into the Number namespace for consistency.
Safe fix: Use Number.parseInt instead.


(lint/style/useNumberNamespace)

</details>

</details>

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T16:33:58Z

Review comments:
diff_hunk: @@ -0,0 +1,47 @@
+import packageInfo from "../../package.json";
+
+class AppConfig {
+  private _domain: string = "707x.in";
+  private _marketName: string = "YTCatalog";
+  private _name: string = packageInfo.name;
+  private _subDomain: string = "ytcatalog";
+  private _version: string = packageInfo.version;
+  private _organization: string = "707x Labs";
+  private _githubRepo: string = "https://github.com/realChakrawarti/yt-catalog";
+
+  get url(): string {
+    return `https://${this._subDomain}.${this._domain}`;
+  }
+
+  get domain(): string {
+    return this._domain;
+  }
+
+  get marketName(): string {
+    return this._marketName;
+  }
+
+  get name(): string {
+    return this._name;
+  }
+
+  get subDomain(): string {
+    return this._subDomain;
+  }
+
+  get version(): string {
+    return this._version;
+  }
+
+  get organization(): string {
+    return this._organization;
+  }
+
+  get githubRepo(): string {
+    return this._githubRepo;
+  }
+}
+
+const appConfig = new AppConfig();
+
+export default appConfig;
path: src/shared/app-config.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Expose configurability through ctor or env vars**

The singleton is hard-wired to production values.  Staging / test environments will have to patch the source code to change `domain`, `subDomain`, etc.  
Consider accepting an optional constructor argument or reading from `process.env` so that values can be overridden without code changes, e.g.:

```ts
const appConfig = new AppConfig({
  domain: process.env.NEXT_PUBLIC_DOMAIN ?? "707x.in",
  subDomain: process.env.NEXT_PUBLIC_SUBDOMAIN ?? "ytcatalog",
});
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T14:42:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+import packageInfo from "../../package.json";
path: src/shared/app-config.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Use JSON import assertions for forward-compatibility**

The bare JSON import relies on the `resolveJsonModule` compiler option and Node ESM interop being aligned. Adding an import assertion makes the intent explicit and prevents accidental breaking changes if loader settings are tweaked later.

```diff
-import packageInfo from "../../package.json";
+import packageInfo from "../../package.json" assert { type: "json" };
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T14:42:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,52 @@
+import { useRef } from "react";
+
+import { indexedDB } from "~/shared/lib/api/dexie";
+import type { History, VideoData } from "~/shared/types-schema/types";
+
+interface UseVideoTrackingProps {
+  playerRef: React.MutableRefObject<YT.Player | null>;
+  video: VideoData;
+}
+
+export function useVideoTracking({ video, playerRef }: UseVideoTrackingProps) {
+  const trackingRef = useRef<NodeJS.Timeout | null>(null);
+  const isPlaying = useRef<boolean>(false);
+
+  function getPercentCompleted(node: YT.Player) {
+    const duration = node.getDuration() || 0;
+    const currentTime = node.getCurrentTime() || 0;
+    const percentCompleted = parseInt(
+      ((currentTime / duration) * 100).toFixed(0)
+    );
+
+    const payload: History = {
+      completed: percentCompleted,
+      duration: currentTime,
+      updatedAt: Date.now(),
+      ...video,
+    };
+
+    return payload;
+  }
+
+  const startTracking = () => {
+    if (trackingRef.current) return;
+
+    trackingRef.current = setInterval(async () => {
+      if (!playerRef.current) return;
+
+      await indexedDB["history"].put(getPercentCompleted(playerRef.current));
+    }, 2_000);
+  };
path: src/widgets/youtube/useVideoTracking.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Mark `isPlaying` when tracking starts**

`isPlaying.current` is flipped to `false` in `stopTracking` but never set back to `true` in `startTracking`, which can cause the resume-from-history logic in the player to run repeatedly.

```diff
     trackingRef.current = setInterval(async () => {
       if (!playerRef.current) return;
       await indexedDB["history"].put(getPercentCompleted(playerRef.current));
     }, 2_000);
+
+    isPlaying.current = true;
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T14:42:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,52 @@
+import { useRef } from "react";
+
+import { indexedDB } from "~/shared/lib/api/dexie";
+import type { History, VideoData } from "~/shared/types-schema/types";
+
+interface UseVideoTrackingProps {
+  playerRef: React.MutableRefObject<YT.Player | null>;
+  video: VideoData;
+}
+
+export function useVideoTracking({ video, playerRef }: UseVideoTrackingProps) {
+  const trackingRef = useRef<NodeJS.Timeout | null>(null);
+  const isPlaying = useRef<boolean>(false);
+
+  function getPercentCompleted(node: YT.Player) {
+    const duration = node.getDuration() || 0;
+    const currentTime = node.getCurrentTime() || 0;
+    const percentCompleted = parseInt(
+      ((currentTime / duration) * 100).toFixed(0)
+    );
+
path: src/widgets/youtube/useVideoTracking.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Handle zero-duration videos & use `Number.parseInt`**

`getDuration()` can return `0` for live streams or prior to initial metadata load.  
Dividing by `0` produces `NaN`, which then pollutes the IndexedDB history.

```diff
-    const percentCompleted = parseInt(
-      ((currentTime / duration) * 100).toFixed(0)
-    );
+    const percentCompleted = duration
+      ? Number.parseInt(((currentTime / duration) * 100).toFixed(0), 10)
+      : 0;
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  function getPercentCompleted(node: YT.Player) {
    const duration = node.getDuration() || 0;
    const currentTime = node.getCurrentTime() || 0;
    const percentCompleted = duration
      ? Number.parseInt(((currentTime / duration) * 100).toFixed(0), 10)
      : 0;
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 18-18: Use Number.parseInt instead of the equivalent global.

ES2015 moved some globals into the Number namespace for consistency.
Safe fix: Use Number.parseInt instead.


(lint/style/useNumberNamespace)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T14:42:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,94 +3,123 @@
 import { YouTubeEmbed } from "@next/third-parties/google";
 import { useEffect, useRef } from "react";
 
+import appConfig from "~/shared/app-config";
 import { indexedDB } from "~/shared/lib/api/dexie";
-import type { History, VideoData } from "~/shared/types-schema/types";
+import type { VideoData } from "~/shared/types-schema/types";
 
-const IframeParams = "rel=0&playsinline=1&origin=https://ytcatalog.707x.in";
+import { useVideoTracking } from "./useVideoTracking";
+
+const IframeParams = `rel=0&playsinline=1&origin=${appConfig.url}`;
 
 export default function YoutubePlayer(
   props: VideoData & { enableJsApi: boolean }
 ) {
   const { enableJsApi, ...video } = props;
+
+  const { videoId, title } = video;
   const containerRef = useRef<HTMLDivElement | null>(null);
-  const trackingRef = useRef<NodeJS.Timeout | null>(null);
   const playerRef = useRef<YT.Player | null>(null);
+  const firstLoad = useRef<boolean>(false);
 
-  const startTracking = () => {
-    if (trackingRef.current) return;
-
-    trackingRef.current = setInterval(async () => {
-      if (!playerRef.current) return;
-
-      const duration = playerRef.current.getDuration() || 0;
-      const currentTime = playerRef.current.getCurrentTime() || 0;
-      const percentCompleted = parseInt(
-        ((currentTime / duration) * 100).toFixed(0)
-      );
-
-      const payload: History = {
-        completed: percentCompleted,
-        duration: currentTime,
-        updatedAt: Date.now(),
-        ...video,
-      };
-
-      await indexedDB["history"].put(payload);
-    }, 2_000);
-  };
-
-  const stopTracking = () => {
-    if (trackingRef.current) {
-      clearInterval(trackingRef.current);
-      trackingRef.current = null;
-    }
-  };
+  const { stopTracking, startTracking, isPlaying } = useVideoTracking({
+    video,
+    playerRef,
+  });
+
+  function getActivePlayers() {
+    return document.querySelectorAll("iframe");
+  }
 
-  function _onStateChange(event: YT.OnStateChangeEvent) {
+  async function _onStateChange(event: YT.OnStateChangeEvent) {
+    const { target, data: playingState } = event;
     const playerState = window.YT.PlayerState;
 
-    switch (event.data) {
-      case playerState.PLAYING:
-        startTracking();
+    const allPlayers = getActivePlayers();
+
+    const filterPlayers = Array.from(allPlayers).filter(
+      (item) => item != target.getIframe()
+    );
+
+    switch (playingState) {
+      case playerState.CUED:
+        stopTracking();
+        firstLoad.current = false;
         break;
       case playerState.PAUSED:
+        stopTracking();
+        break;
       case playerState.ENDED:
         stopTracking();
         break;
+
+      case playerState.PLAYING:
+        // Stop other players
+        filterPlayers.forEach((item) => {
+          playerControl(item, "stopVideo");
+        });
+
+        // firstLoad - The video is being played after loaded previously (after paused or stopped),
+        // isPlaying - Checks whether is video is actively playing, removal re-triggers the playing state causes a loop
+        if (!firstLoad.current && !isPlaying.current) {
+          const playedVideo = await indexedDB["history"].get(videoId);
+          if (playedVideo) {
+            target.seekTo(playedVideo.duration, true);
+            isPlaying.current = true;
+          }
+        }
+        isPlaying.current = true;
+        startTracking();
+        break;
     }
   }
 
+  async function playerControl(
+    iframe: HTMLIFrameElement,
+    action: "playVideo" | "stopVideo" | "pauseVideo" | "destroy"
+  ) {
+    const payload = JSON.stringify({
+      args: [],
+      event: "command",
+      func: action,
+    });
+
+    iframe.contentWindow?.postMessage(payload, "*");
+  }
+
+  // first load
   async function loadIFrameElement() {
     if (!enableJsApi) {
       return;
     }
 
-    if (!playerRef.current) {
-      playerRef.current = await (
-        containerRef.current?.querySelector("lite-youtube") as any
-      )?.getYTPlayer();
+    playerRef.current = await (
+      containerRef.current?.querySelector("lite-youtube") as any
+    )?.getYTPlayer();
 
-      const playing = await indexedDB["history"].get(video.videoId);
+    const playing = await indexedDB["history"].get(video.videoId);
 
-      if (playing?.videoId === video.videoId) {
-        playerRef.current?.seekTo(playing.duration, true);
-      }
+    firstLoad.current = true;
 
+    if (playing?.videoId === videoId) {
       // Set up player event listeners
-      playerRef.current?.addEventListener("onStateChange", _onStateChange);
+      playerRef.current?.seekTo(playing.duration, true);
     }
+    playerRef.current?.addEventListener("onStateChange", _onStateChange);
   }
path: src/widgets/youtube/player.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Event-listener leak across re-renders**

`_onStateChange` is recreated on every render, but the listener is added only once.  
During unmount the cleanup calls `removeEventListener` with a *different* function reference, leaving the real listener hanging and the player never garbage-collected.

Wrap the handler in `useCallback` (or store it in a `useRef`) so the same reference is used for add/remove.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T14:42:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,94 +3,123 @@
 import { YouTubeEmbed } from "@next/third-parties/google";
 import { useEffect, useRef } from "react";
 
+import appConfig from "~/shared/app-config";
 import { indexedDB } from "~/shared/lib/api/dexie";
-import type { History, VideoData } from "~/shared/types-schema/types";
+import type { VideoData } from "~/shared/types-schema/types";
 
-const IframeParams = "rel=0&playsinline=1&origin=https://ytcatalog.707x.in";
+import { useVideoTracking } from "./useVideoTracking";
+
+const IframeParams = `rel=0&playsinline=1&origin=${appConfig.url}`;
 
 export default function YoutubePlayer(
   props: VideoData & { enableJsApi: boolean }
 ) {
   const { enableJsApi, ...video } = props;
+
+  const { videoId, title } = video;
   const containerRef = useRef<HTMLDivElement | null>(null);
-  const trackingRef = useRef<NodeJS.Timeout | null>(null);
   const playerRef = useRef<YT.Player | null>(null);
+  const firstLoad = useRef<boolean>(false);
 
-  const startTracking = () => {
-    if (trackingRef.current) return;
-
-    trackingRef.current = setInterval(async () => {
-      if (!playerRef.current) return;
-
-      const duration = playerRef.current.getDuration() || 0;
-      const currentTime = playerRef.current.getCurrentTime() || 0;
-      const percentCompleted = parseInt(
-        ((currentTime / duration) * 100).toFixed(0)
-      );
-
-      const payload: History = {
-        completed: percentCompleted,
-        duration: currentTime,
-        updatedAt: Date.now(),
-        ...video,
-      };
-
-      await indexedDB["history"].put(payload);
-    }, 2_000);
-  };
-
-  const stopTracking = () => {
-    if (trackingRef.current) {
-      clearInterval(trackingRef.current);
-      trackingRef.current = null;
-    }
-  };
+  const { stopTracking, startTracking, isPlaying } = useVideoTracking({
+    video,
+    playerRef,
+  });
+
+  function getActivePlayers() {
+    return document.querySelectorAll("iframe");
+  }
 
path: src/widgets/youtube/player.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Limit query to YouTube iframes only**

`document.querySelectorAll("iframe")` indiscriminately returns every iframe on the page, so non-YouTube embeds (analytics, ads, etc.) receive YT postMessages.  Narrow the selector:

```diff
-  function getActivePlayers() {
-    return document.querySelectorAll("iframe");
-  }
+  function getActivePlayers() {
+    return document.querySelectorAll('iframe[src*="youtube.com"]');
+  }
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  function getActivePlayers() {
    return document.querySelectorAll('iframe[src*="youtube.com"]');
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T14:42:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,94 +3,123 @@
 import { YouTubeEmbed } from "@next/third-parties/google";
 import { useEffect, useRef } from "react";
 
+import appConfig from "~/shared/app-config";
 import { indexedDB } from "~/shared/lib/api/dexie";
-import type { History, VideoData } from "~/shared/types-schema/types";
+import type { VideoData } from "~/shared/types-schema/types";
 
-const IframeParams = "rel=0&playsinline=1&origin=https://ytcatalog.707x.in";
+import { useVideoTracking } from "./useVideoTracking";
+
+const IframeParams = `rel=0&playsinline=1&origin=${appConfig.url}`;
 
 export default function YoutubePlayer(
   props: VideoData & { enableJsApi: boolean }
 ) {
   const { enableJsApi, ...video } = props;
+
+  const { videoId, title } = video;
   const containerRef = useRef<HTMLDivElement | null>(null);
-  const trackingRef = useRef<NodeJS.Timeout | null>(null);
   const playerRef = useRef<YT.Player | null>(null);
+  const firstLoad = useRef<boolean>(false);
 
-  const startTracking = () => {
-    if (trackingRef.current) return;
-
-    trackingRef.current = setInterval(async () => {
-      if (!playerRef.current) return;
-
-      const duration = playerRef.current.getDuration() || 0;
-      const currentTime = playerRef.current.getCurrentTime() || 0;
-      const percentCompleted = parseInt(
-        ((currentTime / duration) * 100).toFixed(0)
-      );
-
-      const payload: History = {
-        completed: percentCompleted,
-        duration: currentTime,
-        updatedAt: Date.now(),
-        ...video,
-      };
-
-      await indexedDB["history"].put(payload);
-    }, 2_000);
-  };
-
-  const stopTracking = () => {
-    if (trackingRef.current) {
-      clearInterval(trackingRef.current);
-      trackingRef.current = null;
-    }
-  };
+  const { stopTracking, startTracking, isPlaying } = useVideoTracking({
+    video,
+    playerRef,
+  });
+
+  function getActivePlayers() {
+    return document.querySelectorAll("iframe");
+  }
 
-  function _onStateChange(event: YT.OnStateChangeEvent) {
+  async function _onStateChange(event: YT.OnStateChangeEvent) {
+    const { target, data: playingState } = event;
     const playerState = window.YT.PlayerState;
 
-    switch (event.data) {
-      case playerState.PLAYING:
-        startTracking();
+    const allPlayers = getActivePlayers();
+
+    const filterPlayers = Array.from(allPlayers).filter(
+      (item) => item != target.getIframe()
+    );
+
+    switch (playingState) {
+      case playerState.CUED:
+        stopTracking();
+        firstLoad.current = false;
         break;
       case playerState.PAUSED:
+        stopTracking();
+        break;
       case playerState.ENDED:
         stopTracking();
         break;
+
+      case playerState.PLAYING:
+        // Stop other players
+        filterPlayers.forEach((item) => {
+          playerControl(item, "stopVideo");
+        });
+
+        // firstLoad - The video is being played after loaded previously (after paused or stopped),
+        // isPlaying - Checks whether is video is actively playing, removal re-triggers the playing state causes a loop
+        if (!firstLoad.current && !isPlaying.current) {
+          const playedVideo = await indexedDB["history"].get(videoId);
+          if (playedVideo) {
+            target.seekTo(playedVideo.duration, true);
+            isPlaying.current = true;
+          }
+        }
+        isPlaying.current = true;
+        startTracking();
+        break;
     }
   }
 
+  async function playerControl(
+    iframe: HTMLIFrameElement,
+    action: "playVideo" | "stopVideo" | "pauseVideo" | "destroy"
+  ) {
+    const payload = JSON.stringify({
+      args: [],
+      event: "command",
+      func: action,
+    });
+
+    iframe.contentWindow?.postMessage(payload, "*");
+  }
+
+  // first load
   async function loadIFrameElement() {
     if (!enableJsApi) {
       return;
     }
 
-    if (!playerRef.current) {
-      playerRef.current = await (
-        containerRef.current?.querySelector("lite-youtube") as any
-      )?.getYTPlayer();
+    playerRef.current = await (
+      containerRef.current?.querySelector("lite-youtube") as any
+    )?.getYTPlayer();
 
-      const playing = await indexedDB["history"].get(video.videoId);
+    const playing = await indexedDB["history"].get(video.videoId);
 
-      if (playing?.videoId === video.videoId) {
-        playerRef.current?.seekTo(playing.duration, true);
-      }
+    firstLoad.current = true;
 
+    if (playing?.videoId === videoId) {
       // Set up player event listeners
-      playerRef.current?.addEventListener("onStateChange", _onStateChange);
+      playerRef.current?.seekTo(playing.duration, true);
     }
+    playerRef.current?.addEventListener("onStateChange", _onStateChange);
   }
 
   useEffect(() => {
-    return () => stopTracking();
+    return () => {
+      stopTracking();
+      playerRef.current?.removeEventListener("onStateChange", _onStateChange);
+    };
   }, []);
 
   return (
     <div ref={containerRef} onMouseDown={loadIFrameElement}>
       <YouTubeEmbed
         params={enableJsApi ? IframeParams + "&enablejsapi=1" : IframeParams}
-        videoid={video.videoId}
-        playlabel={video.title}
+        videoid={videoId}
+        playlabel={title}
path: src/widgets/youtube/player.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Prefer template literal over string concatenation**

Biome flagged this. A tiny style tweak keeps the codebase consistent:

```diff
-        params={enableJsApi ? IframeParams + "&enablejsapi=1" : IframeParams}
+        params={`${IframeParams}${enableJsApi ? "&enablejsapi=1" : ""}`}
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    <div ref={containerRef} onMouseDown={loadIFrameElement}>
      <YouTubeEmbed
        params={`${IframeParams}${enableJsApi ? "&enablejsapi=1" : ""}`}
        videoid={videoId}
        playlabel={title}
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 120-120: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.


(lint/style/useTemplate)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T14:42:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,94 +3,123 @@
 import { YouTubeEmbed } from "@next/third-parties/google";
 import { useEffect, useRef } from "react";
 
+import appConfig from "~/shared/app-config";
 import { indexedDB } from "~/shared/lib/api/dexie";
-import type { History, VideoData } from "~/shared/types-schema/types";
+import type { VideoData } from "~/shared/types-schema/types";
 
-const IframeParams = "rel=0&playsinline=1&origin=https://ytcatalog.707x.in";
+import { useVideoTracking } from "./useVideoTracking";
+
+const IframeParams = `rel=0&playsinline=1&origin=${appConfig.url}`;
 
 export default function YoutubePlayer(
   props: VideoData & { enableJsApi: boolean }
 ) {
   const { enableJsApi, ...video } = props;
+
+  const { videoId, title } = video;
   const containerRef = useRef<HTMLDivElement | null>(null);
-  const trackingRef = useRef<NodeJS.Timeout | null>(null);
   const playerRef = useRef<YT.Player | null>(null);
+  const firstLoad = useRef<boolean>(false);
 
-  const startTracking = () => {
-    if (trackingRef.current) return;
-
-    trackingRef.current = setInterval(async () => {
-      if (!playerRef.current) return;
-
-      const duration = playerRef.current.getDuration() || 0;
-      const currentTime = playerRef.current.getCurrentTime() || 0;
-      const percentCompleted = parseInt(
-        ((currentTime / duration) * 100).toFixed(0)
-      );
-
-      const payload: History = {
-        completed: percentCompleted,
-        duration: currentTime,
-        updatedAt: Date.now(),
-        ...video,
-      };
-
-      await indexedDB["history"].put(payload);
-    }, 2_000);
-  };
-
-  const stopTracking = () => {
-    if (trackingRef.current) {
-      clearInterval(trackingRef.current);
-      trackingRef.current = null;
-    }
-  };
+  const { stopTracking, startTracking, isPlaying } = useVideoTracking({
+    video,
+    playerRef,
+  });
+
+  function getActivePlayers() {
+    return document.querySelectorAll("iframe");
+  }
 
-  function _onStateChange(event: YT.OnStateChangeEvent) {
+  async function _onStateChange(event: YT.OnStateChangeEvent) {
+    const { target, data: playingState } = event;
     const playerState = window.YT.PlayerState;
 
-    switch (event.data) {
-      case playerState.PLAYING:
-        startTracking();
+    const allPlayers = getActivePlayers();
+
+    const filterPlayers = Array.from(allPlayers).filter(
+      (item) => item != target.getIframe()
+    );
+
+    switch (playingState) {
+      case playerState.CUED:
+        stopTracking();
+        firstLoad.current = false;
         break;
       case playerState.PAUSED:
+        stopTracking();
+        break;
       case playerState.ENDED:
         stopTracking();
         break;
+
+      case playerState.PLAYING:
+        // Stop other players
+        filterPlayers.forEach((item) => {
+          playerControl(item, "stopVideo");
+        });
+
+        // firstLoad - The video is being played after loaded previously (after paused or stopped),
+        // isPlaying - Checks whether is video is actively playing, removal re-triggers the playing state causes a loop
+        if (!firstLoad.current && !isPlaying.current) {
+          const playedVideo = await indexedDB["history"].get(videoId);
+          if (playedVideo) {
+            target.seekTo(playedVideo.duration, true);
+            isPlaying.current = true;
+          }
+        }
+        isPlaying.current = true;
+        startTracking();
+        break;
     }
   }
 
+  async function playerControl(
+    iframe: HTMLIFrameElement,
+    action: "playVideo" | "stopVideo" | "pauseVideo" | "destroy"
+  ) {
+    const payload = JSON.stringify({
+      args: [],
+      event: "command",
+      func: action,
+    });
+
+    iframe.contentWindow?.postMessage(payload, "*");
+  }
path: src/widgets/youtube/player.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Restrict `postMessage` target origin**

Sending commands with `targetOrigin = "*"` allows any embedded attacker iframe to spoof YT messages. Use the iframe’s actual origin to constrain the message.

```diff
-    iframe.contentWindow?.postMessage(payload, "*");
+    const { origin } = new URL(iframe.src);
+    iframe.contentWindow?.postMessage(payload, origin);
```
[security]


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  async function playerControl(
    iframe: HTMLIFrameElement,
    action: "playVideo" | "stopVideo" | "pauseVideo" | "destroy"
  ) {
    const payload = JSON.stringify({
      args: [],
      event: "command",
      func: action,
    });

    const { origin } = new URL(iframe.src);
    iframe.contentWindow?.postMessage(payload, origin);
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T14:42:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,7 +1,9 @@
 import { Metadata } from "next";
 
+import appConfig from "~/shared/app-config";
+
 export const metadata: Metadata = {
-  title: "OpenAPI | YTCatalog",
+  title: `OpenAPI | ${appConfig.url}`,
path: src/app/(views)/openapi/layout.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Inconsistency in title format compared to other layouts**

While other layout files like dashboard and explore use `appConfig.marketName` for the title suffix (e.g., `Dashboard | ${appConfig.marketName}`), this file uses `appConfig.url` instead. This creates inconsistency in how page titles are formatted throughout the application.

Consider changing to:
```tsx
title: `OpenAPI | ${appConfig.marketName}`,
```

Unless there's a specific reason for the OpenAPI page to display the URL rather than the market name in its title.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T14:42:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,8 +3,10 @@ import "slick-carousel/slick/slick-theme.css";
 
 import { Metadata } from "next";
 
+import appConfig from "~/shared/app-config";
+
 export const metadata: Metadata = {
-  title: "Explore | YTCatalog",
+  title: `Explore | ${appConfig.marketName}`,
 };
 
 export default function DashboardLayout({
path: src/app/(views)/explore/layout.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Function name doesn't match the file purpose**

The function is named `DashboardLayout` but this file is for the explore layout. This appears to be a copy-paste error.

Consider renaming to:
```tsx
export default function ExploreLayout({
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T14:42:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,7 +1,7 @@
 import { ComponentProps } from "react";
 
 interface OutLinkProps extends ComponentProps<"a"> {
-  href: `https://${string}`;
+  href: `https://${string}` | string;
path: src/widgets/out-link.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Consider type safety with the broader href type**

While broadening the type to accept any string provides more flexibility, it removes the compile-time safety that ensured only HTTPS links were used. If this component is specifically for external links, consider if there's a middle ground that maintains some safety while allowing for configurable URLs.

If the intent is to support both external and internal links, you might want to rename the component or add a prop that determines the link behavior:

```diff
-interface OutLinkProps extends ComponentProps<"a"> {
-  href: `https://${string}` | string;
+interface OutLinkProps extends ComponentProps<"a"> {
+  href: string;
+  isExternal?: boolean;
+}
+
+export function OutLink({
+  children,
+  href,
+  target = "_blank",
+  isExternal = true,
+  ...rest
+}: OutLinkProps) {
+  return (
+    <a 
+      rel={isExternal ? "noopener noreferrer external" : undefined} 
+      href={href} 
+      target={isExternal ? target : undefined} 
+      {...rest}
+    >
+      {children}
+    </a>
+  );
+}
```


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T14:42:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,47 @@
+import packageInfo from "../../package.json" assert { type: "json" };
+
+class AppConfig {
+  private _domain: string = "707x.in";
+  private _marketName: string = "YTCatalog";
+  private _name: string = packageInfo.name;
+  private _subDomain: string = "ytcatalog";
+  private _version: string = packageInfo.version;
+  private _organization: string = "707x Labs";
+  private _githubRepo: string = "https://github.com/realChakrawarti/yt-catalog";
path: src/shared/app-config.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Remove inferrable type annotations.**

The type annotations on the private fields are redundant as TypeScript can infer them from the assigned values.

```diff
- private _domain: string = "707x.in";
- private _marketName: string = "YTCatalog";
- private _name: string = packageInfo.name;
- private _subDomain: string = "ytcatalog";
- private _version: string = packageInfo.version;
- private _organization: string = "707x Labs";
- private _githubRepo: string = "https://github.com/realChakrawarti/yt-catalog";
+ private _domain = "707x.in";
+ private _marketName = "YTCatalog";
+ private _name = packageInfo.name;
+ private _subDomain = "ytcatalog";
+ private _version = packageInfo.version;
+ private _organization = "707x Labs";
+ private _githubRepo = "https://github.com/realChakrawarti/yt-catalog";
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  private _domain = "707x.in";
  private _marketName = "YTCatalog";
  private _name = packageInfo.name;
  private _subDomain = "ytcatalog";
  private _version = packageInfo.version;
  private _organization = "707x Labs";
  private _githubRepo = "https://github.com/realChakrawarti/yt-catalog";
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 4-4: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 5-5: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 7-7: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 9-9: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

---

[error] 10-10: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.


(lint/style/noInferrableTypes)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T19:56:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,52 @@
+import { useRef } from "react";
+
+import { indexedDB } from "~/shared/lib/api/dexie";
+import type { History, VideoData } from "~/shared/types-schema/types";
+
+interface UseVideoTrackingProps {
+  playerRef: React.MutableRefObject<YT.Player | null>;
+  video: VideoData;
+}
+
+export function useVideoTracking({ video, playerRef }: UseVideoTrackingProps) {
+  const trackingRef = useRef<NodeJS.Timeout | null>(null);
+  const isPlaying = useRef<boolean>(false);
+
+  function getPercentCompleted(node: YT.Player) {
+    const duration = node.getDuration() || 0;
+    const currentTime = node.getCurrentTime() || 0;
+    const percentCompleted = duration
+      ? parseInt(((currentTime / duration) * 100).toFixed(2), 10)
path: src/widgets/youtube/use-video-tracking.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Use Number.parseInt instead of global parseInt.**

For consistency with modern ES standards, prefer using `Number.parseInt` instead of the global `parseInt`.

```diff
- parseInt(((currentTime / duration) * 100).toFixed(2), 10)
+ Number.parseInt(((currentTime / duration) * 100).toFixed(2), 10)
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
      ? Number.parseInt(((currentTime / duration) * 100).toFixed(2), 10)
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 19-19: Use Number.parseInt instead of the equivalent global.

ES2015 moved some globals into the Number namespace for consistency.
Safe fix: Use Number.parseInt instead.


(lint/style/useNumberNamespace)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T19:56:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,52 @@
+import { useRef } from "react";
+
+import { indexedDB } from "~/shared/lib/api/dexie";
+import type { History, VideoData } from "~/shared/types-schema/types";
+
+interface UseVideoTrackingProps {
+  playerRef: React.MutableRefObject<YT.Player | null>;
+  video: VideoData;
+}
+
+export function useVideoTracking({ video, playerRef }: UseVideoTrackingProps) {
+  const trackingRef = useRef<NodeJS.Timeout | null>(null);
+  const isPlaying = useRef<boolean>(false);
+
+  function getPercentCompleted(node: YT.Player) {
+    const duration = node.getDuration() || 0;
+    const currentTime = node.getCurrentTime() || 0;
+    const percentCompleted = duration
+      ? parseInt(((currentTime / duration) * 100).toFixed(2), 10)
+      : 0;
+
+    const payload: History = {
+      completed: percentCompleted,
+      duration: +currentTime.toFixed(2),
+      updatedAt: Date.now(),
+      ...video,
+    };
+
+    return payload;
+  }
+
+  const startTracking = () => {
+    if (trackingRef.current) return;
+
+    trackingRef.current = setInterval(async () => {
+      if (!playerRef.current) return;
+
+      await indexedDB["history"].put(getPercentCompleted(playerRef.current));
+    }, 2_000);
+  };
+
+  const stopTracking = () => {
+    isPlaying.current = false;
+
+    if (trackingRef.current) {
+      clearInterval(trackingRef.current);
+      trackingRef.current = null;
+    }
+  };
path: src/widgets/youtube/use-video-tracking.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add useEffect cleanup for interval.**

The hook should clean up the interval if the component unmounts to prevent memory leaks and unexpected behavior.

```diff
import { useRef, useEffect } from "react";

// ... existing code ...

export function useVideoTracking({ video, playerRef }: UseVideoTrackingProps) {
  // ... existing code ...

  const startTracking = () => {
    // ... existing code ...
  };

  const stopTracking = () => {
    // ... existing code ...
  };

+ useEffect(() => {
+   // Cleanup function for when component unmounts
+   return () => {
+     if (trackingRef.current) {
+       clearInterval(trackingRef.current);
+       trackingRef.current = null;
+     }
+   };
+ }, []);

  return { isPlaying, startTracking, stopTracking };
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import { useRef, useEffect } from "react";

// ... existing code ...

export function useVideoTracking({ video, playerRef }: UseVideoTrackingProps) {
  // ... existing code ...

  const startTracking = () => {
    if (trackingRef.current) return;

    trackingRef.current = setInterval(async () => {
      if (!playerRef.current) return;

      await indexedDB["history"].put(getPercentCompleted(playerRef.current));
    }, 2_000);
  };

  const stopTracking = () => {
    isPlaying.current = false;

    if (trackingRef.current) {
      clearInterval(trackingRef.current);
      trackingRef.current = null;
    }
  };

  useEffect(() => {
    // Cleanup function for when component unmounts
    return () => {
      if (trackingRef.current) {
        clearInterval(trackingRef.current);
        trackingRef.current = null;
      }
    };
  }, []);

  return { isPlaying, startTracking, stopTracking };
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T19:56:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,52 @@
+import { useRef } from "react";
+
+import { indexedDB } from "~/shared/lib/api/dexie";
+import type { History, VideoData } from "~/shared/types-schema/types";
+
+interface UseVideoTrackingProps {
+  playerRef: React.MutableRefObject<YT.Player | null>;
+  video: VideoData;
+}
+
+export function useVideoTracking({ video, playerRef }: UseVideoTrackingProps) {
+  const trackingRef = useRef<NodeJS.Timeout | null>(null);
+  const isPlaying = useRef<boolean>(false);
+
+  function getPercentCompleted(node: YT.Player) {
+    const duration = node.getDuration() || 0;
+    const currentTime = node.getCurrentTime() || 0;
+    const percentCompleted = duration
+      ? parseInt(((currentTime / duration) * 100).toFixed(2), 10)
+      : 0;
+
+    const payload: History = {
+      completed: percentCompleted,
+      duration: +currentTime.toFixed(2),
+      updatedAt: Date.now(),
+      ...video,
+    };
+
+    return payload;
+  }
+
+  const startTracking = () => {
+    if (trackingRef.current) return;
+
+    trackingRef.current = setInterval(async () => {
+      if (!playerRef.current) return;
+
+      await indexedDB["history"].put(getPercentCompleted(playerRef.current));
path: src/widgets/youtube/use-video-tracking.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Add error handling for IndexedDB operations.**

The IndexedDB operation should include error handling to gracefully handle potential failures.

```diff
- await indexedDB["history"].put(getPercentCompleted(playerRef.current));
+ try {
+   await indexedDB["history"].put(getPercentCompleted(playerRef.current));
+ } catch (error) {
+   console.error("Failed to update video history:", error);
+ }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
      try {
        await indexedDB["history"].put(getPercentCompleted(playerRef.current));
      } catch (error) {
        console.error("Failed to update video history:", error);
      }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T19:56:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,94 +3,123 @@
 import { YouTubeEmbed } from "@next/third-parties/google";
 import { useEffect, useRef } from "react";
 
+import appConfig from "~/shared/app-config";
 import { indexedDB } from "~/shared/lib/api/dexie";
-import type { History, VideoData } from "~/shared/types-schema/types";
+import type { VideoData } from "~/shared/types-schema/types";
 
-const IframeParams = "rel=0&playsinline=1&origin=https://ytcatalog.707x.in";
+import { useVideoTracking } from "./use-video-tracking";
+
+const iframeParams = `rel=0&playsinline=1&origin=${appConfig.url}`;
 
 export default function YoutubePlayer(
   props: VideoData & { enableJsApi: boolean }
 ) {
   const { enableJsApi, ...video } = props;
+
+  const { videoId, title } = video;
   const containerRef = useRef<HTMLDivElement | null>(null);
-  const trackingRef = useRef<NodeJS.Timeout | null>(null);
   const playerRef = useRef<YT.Player | null>(null);
+  const firstLoad = useRef<boolean>(false);
 
-  const startTracking = () => {
-    if (trackingRef.current) return;
-
-    trackingRef.current = setInterval(async () => {
-      if (!playerRef.current) return;
-
-      const duration = playerRef.current.getDuration() || 0;
-      const currentTime = playerRef.current.getCurrentTime() || 0;
-      const percentCompleted = parseInt(
-        ((currentTime / duration) * 100).toFixed(0)
-      );
-
-      const payload: History = {
-        completed: percentCompleted,
-        duration: currentTime,
-        updatedAt: Date.now(),
-        ...video,
-      };
-
-      await indexedDB["history"].put(payload);
-    }, 2_000);
-  };
-
-  const stopTracking = () => {
-    if (trackingRef.current) {
-      clearInterval(trackingRef.current);
-      trackingRef.current = null;
-    }
-  };
+  const { stopTracking, startTracking, isPlaying } = useVideoTracking({
+    video,
+    playerRef,
+  });
+
+  function getActivePlayers() {
+    return document.querySelectorAll("iframe");
+  }
 
-  function _onStateChange(event: YT.OnStateChangeEvent) {
+  async function _onStateChange(event: YT.OnStateChangeEvent) {
+    const { target, data: playingState } = event;
     const playerState = window.YT.PlayerState;
 
-    switch (event.data) {
-      case playerState.PLAYING:
-        startTracking();
+    const allPlayers = getActivePlayers();
+
+    const filterPlayers = Array.from(allPlayers).filter(
+      (item) => item != target.getIframe()
+    );
+
+    switch (playingState) {
+      case playerState.CUED:
+        stopTracking();
+        firstLoad.current = false;
         break;
       case playerState.PAUSED:
+        stopTracking();
+        break;
       case playerState.ENDED:
         stopTracking();
         break;
+
+      case playerState.PLAYING:
+        // Stop other players
+        filterPlayers.forEach((item) => {
+          playerControl(item, "stopVideo");
+        });
+
+        // firstLoad - The video is being played after loaded previously (after paused or stopped),
+        // isPlaying - Checks whether is video is actively playing, removal re-triggers the playing state causes a loop
+        if (!firstLoad.current && !isPlaying.current) {
+          const playedVideo = await indexedDB["history"].get(videoId);
+          if (playedVideo) {
+            target.seekTo(playedVideo.duration, true);
+            isPlaying.current = true;
+          }
+        }
+        isPlaying.current = true;
+        startTracking();
+        break;
     }
   }
 
+  async function playerControl(
+    iframe: HTMLIFrameElement,
+    action: "playVideo" | "stopVideo" | "pauseVideo" | "destroy"
+  ) {
+    const payload = JSON.stringify({
+      args: [],
+      event: "command",
+      func: action,
+    });
+
+    iframe.contentWindow?.postMessage(payload, "*");
+  }
+
+  // first load
   async function loadIFrameElement() {
     if (!enableJsApi) {
       return;
     }
 
-    if (!playerRef.current) {
-      playerRef.current = await (
-        containerRef.current?.querySelector("lite-youtube") as any
-      )?.getYTPlayer();
+    playerRef.current = await (
+      containerRef.current?.querySelector("lite-youtube") as any
+    )?.getYTPlayer();
 
-      const playing = await indexedDB["history"].get(video.videoId);
+    const playing = await indexedDB["history"].get(videoId);
 
-      if (playing?.videoId === video.videoId) {
-        playerRef.current?.seekTo(playing.duration, true);
-      }
+    firstLoad.current = true;
 
-      // Set up player event listeners
-      playerRef.current?.addEventListener("onStateChange", _onStateChange);
+    if (playing?.videoId === videoId) {
+      playerRef.current?.seekTo(playing.duration, true);
     }
+    // Set up player event listeners
+    playerRef.current?.addEventListener("onStateChange", _onStateChange);
   }
 
   useEffect(() => {
-    return () => stopTracking();
+    return () => {
+      stopTracking();
+      playerRef.current?.removeEventListener("onStateChange", _onStateChange);
+    };
   }, []);
 
   return (
     <div ref={containerRef} onMouseDown={loadIFrameElement}>
       <YouTubeEmbed
-        params={enableJsApi ? IframeParams + "&enablejsapi=1" : IframeParams}
-        videoid={video.videoId}
-        playlabel={video.title}
+        params={enableJsApi ? iframeParams + "&enablejsapi=1" : iframeParams}
path: src/widgets/youtube/player.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Multiple `onStateChange` listeners accumulate on repeated clicks**

Every time the user clicks the thumbnail (`onMouseDown`), `loadIFrameElement`
runs and attaches a new listener without removing the previous one. After a few
click-pause cycles the same event fires N times.

Guard with a boolean or remove the listener first:

```diff
-iframePlayer?.addEventListener("onStateChange", _onStateChange);
+if (playerRef.current && !_listenerAttached.current) {
+  playerRef.current.addEventListener("onStateChange", _onStateChange);
+  _listenerAttached.current = true;
+}
```

Add `_listenerAttached = useRef(false)` beside the other refs.  



> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 120-120: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.


(lint/style/useTemplate)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T19:56:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,94 +3,123 @@
 import { YouTubeEmbed } from "@next/third-parties/google";
 import { useEffect, useRef } from "react";
 
+import appConfig from "~/shared/app-config";
 import { indexedDB } from "~/shared/lib/api/dexie";
-import type { History, VideoData } from "~/shared/types-schema/types";
+import type { VideoData } from "~/shared/types-schema/types";
 
-const IframeParams = "rel=0&playsinline=1&origin=https://ytcatalog.707x.in";
+import { useVideoTracking } from "./use-video-tracking";
+
+const iframeParams = `rel=0&playsinline=1&origin=${appConfig.url}`;
 
 export default function YoutubePlayer(
   props: VideoData & { enableJsApi: boolean }
 ) {
   const { enableJsApi, ...video } = props;
+
+  const { videoId, title } = video;
   const containerRef = useRef<HTMLDivElement | null>(null);
-  const trackingRef = useRef<NodeJS.Timeout | null>(null);
   const playerRef = useRef<YT.Player | null>(null);
+  const firstLoad = useRef<boolean>(false);
 
-  const startTracking = () => {
-    if (trackingRef.current) return;
-
-    trackingRef.current = setInterval(async () => {
-      if (!playerRef.current) return;
-
-      const duration = playerRef.current.getDuration() || 0;
-      const currentTime = playerRef.current.getCurrentTime() || 0;
-      const percentCompleted = parseInt(
-        ((currentTime / duration) * 100).toFixed(0)
-      );
-
-      const payload: History = {
-        completed: percentCompleted,
-        duration: currentTime,
-        updatedAt: Date.now(),
-        ...video,
-      };
-
-      await indexedDB["history"].put(payload);
-    }, 2_000);
-  };
-
-  const stopTracking = () => {
-    if (trackingRef.current) {
-      clearInterval(trackingRef.current);
-      trackingRef.current = null;
-    }
-  };
+  const { stopTracking, startTracking, isPlaying } = useVideoTracking({
+    video,
+    playerRef,
+  });
+
+  function getActivePlayers() {
+    return document.querySelectorAll("iframe");
+  }
path: src/widgets/youtube/player.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

_⚠️ Potential issue_

**Return a typed `NodeListOf<HTMLIFrameElement>` and constrain the selector to YouTube iframes**

`querySelectorAll("iframe")` returns a `NodeListOf<Element>`.  
A subsequent call `playerControl(item, …)` expects an `HTMLIFrameElement`, which produces a compile-time type error (`Element` → `HTMLIFrameElement`).  
At the same time, limiting the selector to YouTube frames fixes the over-broadcasting issue already highlighted in earlier reviews.

```diff
-function getActivePlayers() {
-  return document.querySelectorAll("iframe");
-}
+function getActivePlayers(): NodeListOf<HTMLIFrameElement> {
+  return document.querySelectorAll(
+    'iframe[src*="youtube.com"]'
+  ) as NodeListOf<HTMLIFrameElement>;
+}
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  function getActivePlayers(): NodeListOf<HTMLIFrameElement> {
    return document.querySelectorAll(
      'iframe[src*="youtube.com"]'
    ) as NodeListOf<HTMLIFrameElement>;
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-05T20:06:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,65 @@
+import { useEffect, useRef } from "react";
+
+import { indexedDB } from "~/shared/lib/api/dexie";
+import type { History, VideoData } from "~/shared/types-schema/types";
+
+interface UseVideoTrackingProps {
+  playerRef: React.MutableRefObject<YT.Player | null>;
+  video: VideoData;
+}
+
+export function useVideoTracking({ video, playerRef }: UseVideoTrackingProps) {
+  const trackingRef = useRef<NodeJS.Timeout | null>(null);
+  const isPlaying = useRef<boolean>(false);
+
+  function getPercentCompleted(node: YT.Player) {
+    const duration = node.getDuration() || 0;
+    const currentTime = node.getCurrentTime() || 0;
+    const percentCompleted = duration
+      ? parseInt(((currentTime / duration) * 100).toFixed(2), 10)
+      : 0;
+
+    const payload: History = {
+      completed: percentCompleted,
+      duration: +currentTime.toFixed(2),
+      updatedAt: Date.now(),
+      ...video,
+    };
+
+    return payload;
+  }
+
+  const updateProgress = async (player: YT.Player) => {
+    await indexedDB["history"].put(getPercentCompleted(player));
+  };
+
+  const startTracking = () => {
+    if (trackingRef.current) return;
+
+    trackingRef.current = setInterval(async () => {
+      if (!playerRef.current) return;
+
+      await updateProgress(playerRef.current);
+    }, 2_000);
+  };
path: src/widgets/youtube/use-video-tracking.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Tiny optimisation – mark `startTracking` as idempotent & set playing flag**

`startTracking` early-returns when the interval exists 👍.  
Consider also toggling `isPlaying.current = true` here for symmetry with `stopTracking()` so callers don’t have to remember to do it themselves.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T16:33:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,65 @@
+import { useEffect, useRef } from "react";
+
+import { indexedDB } from "~/shared/lib/api/dexie";
+import type { History, VideoData } from "~/shared/types-schema/types";
+
+interface UseVideoTrackingProps {
+  playerRef: React.MutableRefObject<YT.Player | null>;
+  video: VideoData;
+}
+
+export function useVideoTracking({ video, playerRef }: UseVideoTrackingProps) {
+  const trackingRef = useRef<NodeJS.Timeout | null>(null);
+  const isPlaying = useRef<boolean>(false);
+
+  function getPercentCompleted(node: YT.Player) {
+    const duration = node.getDuration() || 0;
+    const currentTime = node.getCurrentTime() || 0;
+    const percentCompleted = duration
+      ? parseInt(((currentTime / duration) * 100).toFixed(2), 10)
+      : 0;
path: src/widgets/youtube/use-video-tracking.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Use `Number.parseFloat` (or `Number.parseInt`) and avoid precision loss**

`toFixed(2)` already produces a string with 2-decimal precision. Passing that through `parseInt` truncates everything after the decimal point, defeating the purpose of `toFixed`.  
At the same time the lint error points out the prefer-namespace rule.

```diff
-      ? parseInt(((currentTime / duration) * 100).toFixed(2), 10)
+      ? Number.parseFloat(((currentTime / duration) * 100).toFixed(2))
```

If you really only need an integer percentage, you can drop `toFixed` entirely and use `Math.floor` for clarity.  
Either way, switching to the `Number` namespace satisfies Biome.




<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    const percentCompleted = duration
      ? Number.parseFloat(((currentTime / duration) * 100).toFixed(2))
      : 0;
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 19-19: Use Number.parseInt instead of the equivalent global.

ES2015 moved some globals into the Number namespace for consistency.
Safe fix: Use Number.parseInt instead.


(lint/style/useNumberNamespace)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T16:33:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,94 +3,131 @@
 import { YouTubeEmbed } from "@next/third-parties/google";
 import { useEffect, useRef } from "react";
 
+import appConfig from "~/shared/app-config";
 import { indexedDB } from "~/shared/lib/api/dexie";
-import type { History, VideoData } from "~/shared/types-schema/types";
+import type { VideoData } from "~/shared/types-schema/types";
 
-const IframeParams = "rel=0&playsinline=1&origin=https://ytcatalog.707x.in";
+import { useVideoTracking } from "./use-video-tracking";
+
+const iframeParams = `rel=0&playsinline=1&origin=${appConfig.url}`;
 
 export default function YoutubePlayer(
   props: VideoData & { enableJsApi: boolean }
 ) {
   const { enableJsApi, ...video } = props;
+
+  const { videoId, title } = video;
   const containerRef = useRef<HTMLDivElement | null>(null);
-  const trackingRef = useRef<NodeJS.Timeout | null>(null);
   const playerRef = useRef<YT.Player | null>(null);
+  const firstLoad = useRef<boolean>(false);
 
-  const startTracking = () => {
-    if (trackingRef.current) return;
-
-    trackingRef.current = setInterval(async () => {
-      if (!playerRef.current) return;
-
-      const duration = playerRef.current.getDuration() || 0;
-      const currentTime = playerRef.current.getCurrentTime() || 0;
-      const percentCompleted = parseInt(
-        ((currentTime / duration) * 100).toFixed(0)
-      );
-
-      const payload: History = {
-        completed: percentCompleted,
-        duration: currentTime,
-        updatedAt: Date.now(),
-        ...video,
-      };
-
-      await indexedDB["history"].put(payload);
-    }, 2_000);
-  };
-
-  const stopTracking = () => {
-    if (trackingRef.current) {
-      clearInterval(trackingRef.current);
-      trackingRef.current = null;
-    }
-  };
+  const { stopTracking, startTracking, isPlaying } = useVideoTracking({
+    video,
+    playerRef,
+  });
+
+  function getActivePlayers() {
+    return document.querySelectorAll("iframe");
+  }
 
-  function _onStateChange(event: YT.OnStateChangeEvent) {
+  async function _onStateChange(event: YT.OnStateChangeEvent) {
+    const { target, data: playingState } = event;
     const playerState = window.YT.PlayerState;
 
-    switch (event.data) {
-      case playerState.PLAYING:
-        startTracking();
+    const allPlayers = getActivePlayers();
+
+    const filterPlayers = Array.from(allPlayers).filter(
+      (item) => item != target.getIframe()
+    );
+
+    switch (playingState) {
+      case playerState.CUED:
+        stopTracking();
+        firstLoad.current = false;
         break;
       case playerState.PAUSED:
+        stopTracking();
+        break;
       case playerState.ENDED:
         stopTracking();
         break;
+
+      case playerState.PLAYING:
+        // Stop other players
+        filterPlayers.forEach((item) => {
+          playerControl(item, "stopVideo");
+        });
+
+        // firstLoad - The video is being played after loaded previously (after paused or stopped),
+        // isPlaying - Checks whether the video is actively playing,
+        // removal re-triggers the playing state causes a loop
+        if (!firstLoad.current && !isPlaying.current) {
+          const playedVideo = await indexedDB["history"].get(videoId);
+          if (playedVideo) {
+            target.seekTo(playedVideo.duration, true);
+            isPlaying.current = true;
+          }
+        }
+        isPlaying.current = true;
+        startTracking();
+        break;
+      case playerState.BUFFERING:
+        const seekedTime = target.getCurrentTime();
+        if (seekedTime > 0) {
+          target.seekTo(seekedTime, true);
+          isPlaying.current = true;
+        }
+        break;
path: src/widgets/youtube/player.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Wrap `seekedTime` in a block to satisfy “noSwitchDeclarations” rule**

Declaring a `const` directly inside a `case` may leak the binding to the next clauses. Enclose the `BUFFERING` branch in its own block:

```diff
-      case playerState.BUFFERING:
-        const seekedTime = target.getCurrentTime();
-        if (seekedTime > 0) {
-          target.seekTo(seekedTime, true);
-          isPlaying.current = true;
-        }
-        break;
+      case playerState.BUFFERING: {
+        const seekedTime = target.getCurrentTime();
+        if (seekedTime > 0) {
+          target.seekTo(seekedTime, true);
+          isPlaying.current = true;
+        }
+        break;
+      }
```

This removes the static-analysis warning and future-proofs the switch.




<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
      case playerState.BUFFERING: {
        const seekedTime = target.getCurrentTime();
        if (seekedTime > 0) {
          target.seekTo(seekedTime, true);
          isPlaying.current = true;
        }
        break;
      }
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 75-75: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.


(lint/correctness/noSwitchDeclarations)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T16:33:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-05T14:35:53Z
body: [vc]: #8+5xTHhNh96BG2WoVB5DWWt8GNgoIsOmsj3tbRJdwns=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJpbmdlc3QiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcmVhbGNoYWtyYXdhcnRpcy1wcm9qZWN0cy9pbmdlc3QvQlRTVEVMUER4RzNTdkdHMVhzMlBMdWExQnloRiIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwicm9vdERpcmVjdG9yeSI6bnVsbCwicHJldmlld1VybCI6ImluZ2VzdC1naXQtZmVhdC1vbmUtcGxheWVyLXJlYWxjaGFrcmF3YXJ0aXMtcHJvamVjdHMudmVyY2VsLmFwcCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)


<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| **ingest** | ⬜️ Ignored ([Inspect](https://vercel.com/realchakrawartis-projects/ingest/BTSTELPDxG3SvGG1Xs2PLua1ByhF)) | [Visit Preview](https://ingest-git-feat-one-player-realchakrawartis-projects.vercel.app) | May 6, 2025 4:30pm |
</details>


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-05T14:35:56Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes introduce a centralized configuration system for application-wide constants such as the market name, domain, app name, subdomain, version, organization, and GitHub repository URL. A new `AppConfig` class encapsulates these details, sourcing some values dynamically from `package.json`. Multiple components and pages are updated to use values from this shared configuration (`appConfig`) instead of hardcoded strings for branding, URLs, and metadata. This affects metadata titles, Open Graph fields, UI text, share links, and footer information. Additionally, a new React hook for YouTube video tracking is added, and the YouTube player is refactored to use it, improving modularity and playback state management. Minor type and function signature adjustments are also made.

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=realChakrawarti/ingest&utm_content=178):

> ‼️ **IMPORTANT**
> Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR