diff --git a/packages/react/src/components/channel/ChannelCard.tsx b/packages/react/src/components/channel/ChannelCard.tsx
new file mode 100644
index 000000000..06974c38a
--- /dev/null
+++ b/packages/react/src/components/channel/ChannelCard.tsx
@@ -0,0 +1,88 @@
+import { formatCount } from "@/lib/time";
+import { useFavoriteMutation, useFavorites } from "@/services/user.service";
+import { Badge } from "@/shadcn/ui/badge";
+import { Button } from "@/shadcn/ui/button";
+import { useMemo } from "react";
+import { Link } from "react-router-dom";
+
+interface ChannelCardProps extends Channel {}
+
+export function ChannelCard({
+ id,
+ name,
+ photo,
+ subscriber_count,
+ video_count,
+ clip_count,
+ top_topics,
+ twitter,
+ twitch,
+}: ChannelCardProps) {
+ const { mutate, isLoading: mutateLoading } = useFavoriteMutation();
+ const { data } = useFavorites();
+ const isInFavorite = useMemo(
+ () => data?.some((channel) => id === channel.id),
+ [data],
+ );
+
+ return (
+ // Set min-height because react-virtuoso will break if the height is not fixed
+
+
+
{name}
+
+
+ {formatCount(subscriber_count ?? 0)} subscribers
+
+
+ {video_count ?? "0"} videos
+ /
+ {clip_count ?? "0"} clips
+
+
+ {top_topics &&
{top_topics[0]}}
+
+
+
+
+ {twitter && (
+
+ )}
+ {twitch && (
+
+ )}
+
+
+ );
+}
diff --git a/packages/react/src/routes/channelsOrg.tsx b/packages/react/src/routes/channelsOrg.tsx
new file mode 100644
index 000000000..5b2bb5f5a
--- /dev/null
+++ b/packages/react/src/routes/channelsOrg.tsx
@@ -0,0 +1,25 @@
+import { ChannelCard } from "@/components/channel/ChannelCard";
+import { useChannels } from "@/services/channel.service";
+import { useParams, useSearchParams } from "react-router-dom";
+import { VirtuosoGrid } from "react-virtuoso";
+
+export function ChannelsOrg() {
+ const { org } = useParams();
+ // const [org, setOrg] = useAtom(orgAtom);
+
+ const { data: channels, fetchNextPage: fetchChannels } = useChannels({ org, sort: 'suborg' });
+
+ return (
+
+ }
+ endReached={async () => {
+ await fetchChannels();
+ }}
+ />
+
+ );
+}
diff --git a/packages/react/src/routes/router.tsx b/packages/react/src/routes/router.tsx
index 9da67222e..fd5f72c23 100644
--- a/packages/react/src/routes/router.tsx
+++ b/packages/react/src/routes/router.tsx
@@ -1,7 +1,10 @@
import Kitchensink from "@/Kitchensink";
-import { Outlet, createBrowserRouter, redirect } from "react-router-dom";
+import { Navigate, Outlet, createBrowserRouter, redirect } from "react-router-dom";
import { Home } from "@/routes/home";
import { Login } from "@/routes/login";
+import { ChannelsOrg } from "./channelsOrg";
+import { useAtomValue } from "jotai";
+import { orgAtom } from "@/store/org";
const settings = {} as any; // TODO: replace with your actual settings store
const site = {} as any; // TODO: replace with your actual site store
@@ -47,7 +50,7 @@ const router = createBrowserRouter([
},
{
path: "/org/:org/channels",
- element: Channels_Org
,
+ element: ,
},
{
path: "/channel/:id",
@@ -130,8 +133,9 @@ const router = createBrowserRouter([
export default router;
function RedirectToChannelsOrg() {
+ const org = useAtomValue(orgAtom);
// Replace with your logic
- return Redirecting to Channels_Org...
;
+ return ;
}
function Channel() {
diff --git a/packages/react/src/services/channel.service.ts b/packages/react/src/services/channel.service.ts
new file mode 100644
index 000000000..2a60992c4
--- /dev/null
+++ b/packages/react/src/services/channel.service.ts
@@ -0,0 +1,52 @@
+import { useClient } from "@/hooks/useClient";
+import {
+ UseInfiniteQueryOptions,
+ UseQueryOptions,
+ useInfiniteQuery,
+ useQuery,
+} from "@tanstack/react-query";
+import { AxiosError } from "axios";
+
+interface UseChannelsParams {
+ limit?: number;
+ offset?: number;
+ sort?: string;
+ order?: "asc" | "desc";
+ type?: ChannelType;
+ org?: string;
+ suborg?: string;
+ lang?: string;
+}
+
+export function useChannels(
+ params: UseChannelsParams,
+ config?: UseInfiniteQueryOptions,
+) {
+ const client = useClient();
+
+ return useInfiniteQuery({
+ queryKey: ["channels", params],
+ queryFn: async ({ pageParam = 0 }) =>
+ (
+ await client("/channels", {
+ params: { ...params, offset: pageParam },
+ })
+ ).data,
+ getNextPageParam: (lastPage, allPages) =>
+ lastPage.length ? allPages.flat().length : undefined,
+ ...config,
+ });
+}
+
+export function useChannel(
+ channelId: string,
+ config?: UseQueryOptions,
+) {
+ const client = useClient();
+
+ return useQuery(
+ ["channel", channelId],
+ async () => (await client(`/channels/${channelId}`)).data,
+ config,
+ );
+}
diff --git a/packages/react/src/services/user.service.ts b/packages/react/src/services/user.service.ts
new file mode 100644
index 000000000..bea569c18
--- /dev/null
+++ b/packages/react/src/services/user.service.ts
@@ -0,0 +1,54 @@
+import { useClient } from "@/hooks/useClient";
+import {
+ UseMutationOptions,
+ UseQueryOptions,
+ useMutation,
+ useQuery,
+ useQueryClient,
+} from "@tanstack/react-query";
+import { AxiosError } from "axios";
+
+export function useFavorites(
+ config?: UseQueryOptions,
+) {
+ const client = useClient();
+
+ return useQuery(
+ ["user", "favorites"],
+ async () => (await client("/users/favorites")).data,
+ config,
+ );
+}
+
+interface FavoriteMutationPayload {
+ op: "add" | "remove";
+ channel_id: string;
+}
+
+export function useFavoriteMutation(
+ config?: UseMutationOptions<
+ FavoriteChannel[],
+ AxiosError,
+ FavoriteMutationPayload[]
+ >,
+) {
+ const queryClient = useQueryClient();
+ const client = useClient();
+
+ return useMutation(
+ async (payload) =>
+ (
+ await client("/users/favorites", {
+ method: "PATCH",
+ data: payload,
+ })
+ ).data,
+ {
+ ...config,
+ onSuccess: (res, ...args) => {
+ queryClient.setQueryData(["user", "favorites"], res);
+ if (config?.onSuccess) config?.onSuccess(res, ...args);
+ },
+ },
+ );
+}
diff --git a/packages/react/src/store/org.ts b/packages/react/src/store/org.ts
new file mode 100644
index 000000000..ca2c3abd2
--- /dev/null
+++ b/packages/react/src/store/org.ts
@@ -0,0 +1,3 @@
+import { atomWithStorage } from "jotai/utils";
+
+export const orgAtom = atomWithStorage('org', 'Hololive');
\ No newline at end of file
diff --git a/packages/react/src/store/settings.ts b/packages/react/src/store/settings.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/react/src/types/channel.d.ts b/packages/react/src/types/channel.d.ts
index 5e487a634..1f1cc1477 100644
--- a/packages/react/src/types/channel.d.ts
+++ b/packages/react/src/types/channel.d.ts
@@ -13,12 +13,35 @@ interface Channel extends ChannelBase {
org: string | null;
suborg: string | null;
banner: string | null;
+ thumbnail: string | null;
twitter: string | null;
+ twitch: string | null;
video_count: string | null;
subscriber_count: string | null;
view_count: string | null;
- cilp_count: string | null;
+ clip_count: string | null;
lang: string | null;
published_at: string;
inactive: boolean;
+ yt_uploads_id: string | null;
+ top_topics: string[] | null;
+ yt_handle: string[] | null;
+ yt_name_history: string[] | null;
+ group: string | null;
}
+
+type FavoriteChannel = Pick<
+ Channel,
+ | "clip_count"
+ | "english_name"
+ | "group"
+ | "id"
+ | "inactive"
+ | "name"
+ | "org"
+ | "photo"
+ | "subscriber_count"
+ | "twitter"
+ | "type"
+ | "video_count"
+>;