Skip to content

Commit

Permalink
Add ChannelCard / fix types
Browse files Browse the repository at this point in the history
  • Loading branch information
P-man2976 committed Oct 14, 2023
1 parent 6761297 commit a848623
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 4 deletions.
88 changes: 88 additions & 0 deletions packages/react/src/components/channel/ChannelCard.tsx
Original file line number Diff line number Diff line change
@@ -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
<div className="w-full h-full min-h-[24rem] flex flex-col gap-2 items-center p-4 bg-base-3 rounded-md">
<img className="w-24 h-24 rounded-full" src={photo ?? ""} />
<div className="font-xl font-bold text-center line-clamp-2">{name}</div>
<div className="flex flex-col items-center">
<div className="text-sm text-base-11 whitespace-nowrap">
{formatCount(subscriber_count ?? 0)} subscribers
</div>
<div className="flex flex-wrap justify-center gap-x-1 gap-y-0 text-sm text-base-11">
<span className="whitespace-nowrap">{video_count ?? "0"} videos</span>
<span>/</span>
<span className="whitespace-nowrap">{clip_count ?? "0"} clips</span>
</div>
</div>
{top_topics && <Badge variant="outline" className="capitalize">{top_topics[0]}</Badge>}
<div className="flex grow" />
<Button
className="w-full"
variant={isInFavorite ? "outline" : "secondary"}
disabled={mutateLoading}
onClick={() =>
mutate([
{
op: isInFavorite ? "remove" : "add",
channel_id: id,
},
])
}
>
{isInFavorite ? (
<div className="i-heroicons:heart-solid" />
) : (
<div className="i-heroicons:heart" />
)}
{isInFavorite ? "Unfavorite" : "Favorite"}
</Button>
<div className="w-full flex gap-2">
<Button asChild className="w-full" variant="ghost" size="icon-lg">
<Link to={`https://www.youtube.com/channel/${id}`} target="_blank">
<div className="i-lucide:youtube" />
</Link>
</Button>
{twitter && (
<Button asChild className="w-full" variant="ghost" size="icon-lg">
<Link to={`https://x.com/${twitter}`} target="_blank">
<div className="i-lucide:twitter" />
</Link>
</Button>
)}
{twitch && (
<Button asChild className="w-full" variant="ghost" size="icon-lg">
<Link to={`https://twitch.tv/${twitch}`} target="_blank">
<div className="i-lucide:twitch" />
</Link>
</Button>
)}
</div>
</div>
);
}
25 changes: 25 additions & 0 deletions packages/react/src/routes/channelsOrg.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="w-full h-full p-4 md:p-8">
<VirtuosoGrid
useWindowScroll
listClassName="w-full grid grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 gap-x-4 gap-y-6"
data={channels?.pages.flat() ?? []}
itemContent={(_, channel) => <ChannelCard {...channel} />}
endReached={async () => {
await fetchChannels();
}}
/>
</div>
);
}
10 changes: 7 additions & 3 deletions packages/react/src/routes/router.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -47,7 +50,7 @@ const router = createBrowserRouter([
},
{
path: "/org/:org/channels",
element: <div>Channels_Org</div>,
element: <ChannelsOrg />,
},
{
path: "/channel/:id",
Expand Down Expand Up @@ -130,8 +133,9 @@ const router = createBrowserRouter([
export default router;

function RedirectToChannelsOrg() {
const org = useAtomValue(orgAtom);
// Replace with your logic
return <div>Redirecting to Channels_Org...</div>;
return <Navigate to={`/org/${org}/channels`} />;
}

function Channel() {
Expand Down
52 changes: 52 additions & 0 deletions packages/react/src/services/channel.service.ts
Original file line number Diff line number Diff line change
@@ -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<Channel[], AxiosError>,
) {
const client = useClient();

return useInfiniteQuery<Channel[], AxiosError>({
queryKey: ["channels", params],
queryFn: async ({ pageParam = 0 }) =>
(
await client<Channel[]>("/channels", {
params: { ...params, offset: pageParam },
})
).data,
getNextPageParam: (lastPage, allPages) =>
lastPage.length ? allPages.flat().length : undefined,
...config,
});
}

export function useChannel(
channelId: string,
config?: UseQueryOptions<Channel, AxiosError>,
) {
const client = useClient();

return useQuery<Channel, AxiosError>(
["channel", channelId],
async () => (await client<Channel>(`/channels/${channelId}`)).data,
config,
);
}
54 changes: 54 additions & 0 deletions packages/react/src/services/user.service.ts
Original file line number Diff line number Diff line change
@@ -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<FavoriteChannel[], AxiosError>,
) {
const client = useClient();

return useQuery<FavoriteChannel[], AxiosError>(
["user", "favorites"],
async () => (await client<FavoriteChannel[]>("/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<FavoriteChannel[], AxiosError, FavoriteMutationPayload[]>(
async (payload) =>
(
await client<FavoriteChannel[]>("/users/favorites", {
method: "PATCH",
data: payload,
})
).data,
{
...config,
onSuccess: (res, ...args) => {
queryClient.setQueryData(["user", "favorites"], res);
if (config?.onSuccess) config?.onSuccess(res, ...args);
},
},
);
}
3 changes: 3 additions & 0 deletions packages/react/src/store/org.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { atomWithStorage } from "jotai/utils";

export const orgAtom = atomWithStorage('org', 'Hololive');
Empty file.
25 changes: 24 additions & 1 deletion packages/react/src/types/channel.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>;

0 comments on commit a848623

Please sign in to comment.