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" +>;