Split loaders for user list

This commit is contained in:
Zoe Roux 2024-05-20 21:55:03 +02:00
parent 2a1b805a7f
commit f3fb96504a
No known key found for this signature in database
5 changed files with 75 additions and 44 deletions

View File

@ -24,6 +24,7 @@ import { Image, type ImageProps, View, type ViewStyle } from "react-native";
import { type Stylable, px, useYoshiki } from "yoshiki/native"; import { type Stylable, px, useYoshiki } from "yoshiki/native";
import { Icon } from "./icons"; import { Icon } from "./icons";
import { P } from "./text"; import { P } from "./text";
import { Skeleton } from "./skeleton";
const stringToColor = (string: string) => { const stringToColor = (string: string) => {
let hash = 0; let hash = 0;
@ -40,7 +41,7 @@ const stringToColor = (string: string) => {
return color; return color;
}; };
export const Avatar = forwardRef< const AvatarC = forwardRef<
View, View,
{ {
src?: string; src?: string;
@ -48,12 +49,11 @@ export const Avatar = forwardRef<
size?: number; size?: number;
placeholder?: string; placeholder?: string;
color?: string; color?: string;
isLoading?: boolean;
fill?: boolean; fill?: boolean;
as?: ComponentType<{ style?: ViewStyle } & RefAttributes<View>>; as?: ComponentType<{ style?: ViewStyle } & RefAttributes<View>>;
} & Stylable } & Stylable
>(function Avatar( >(function AvatarI(
{ src, alt, size = px(24), color, placeholder, isLoading = false, fill = false, as, ...props }, { src, alt, size = px(24), color, placeholder, fill = false, as, ...props },
ref, ref,
) { ) {
const { css, theme } = useYoshiki(); const { css, theme } = useYoshiki();
@ -106,3 +106,22 @@ export const Avatar = forwardRef<
</Container> </Container>
); );
}); });
const AvatarLoader = ({ size = px(24), ...props }: { size?: number }) => {
const { css } = useYoshiki();
return (
<Skeleton
variant="round"
{...css(
{
height: size,
width: size,
},
props,
)}
/>
);
};
export const Avatar = { ...AvatarC, Loader: AvatarLoader };

View File

@ -45,7 +45,7 @@ type IconProps = {
export const Icon = ({ icon: Icon, color, size = 24, ...props }: IconProps) => { export const Icon = ({ icon: Icon, color, size = 24, ...props }: IconProps) => {
const { css, theme } = useYoshiki(); const { css, theme } = useYoshiki();
const computed = css( const computed = css(
{ width: size, height: size, fill: color ?? theme.contrast } as any, { width: size, height: size, fill: color ?? theme.contrast, flexShrink: 0 } as any,
props, props,
) as any; ) as any;

View File

@ -23,7 +23,7 @@ import { Alert, Avatar, Icon, IconButton, Menu, P, Skeleton, tooltip, ts } from
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { px, useYoshiki } from "yoshiki/native"; import { px, useYoshiki } from "yoshiki/native";
import type { Layout, WithLoading } from "../fetch"; import type { Layout } from "../fetch";
import { InfiniteFetch } from "../fetch-infinite"; import { InfiniteFetch } from "../fetch-infinite";
import { SettingsContainer } from "../settings/base"; import { SettingsContainer } from "../settings/base";
@ -36,20 +36,19 @@ import Verifed from "@material-symbols/svg-400/rounded/verified_user.svg";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
export const UserGrid = ({ export const UserGrid = ({
isLoading,
id, id,
username, username,
avatar, avatar,
isAdmin, isAdmin,
isVerified, isVerified,
...props ...props
}: WithLoading<{ }: {
id: string; id: string;
username: string; username: string;
avatar: string; avatar: string;
isAdmin: boolean; isAdmin: boolean;
isVerified: boolean; isVerified: boolean;
}>) => { }) => {
const { css } = useYoshiki(); const { css } = useYoshiki();
const { t } = useTranslation(); const { t } = useTranslation();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -66,11 +65,10 @@ export const UserGrid = ({
return ( return (
<View {...css({ alignItems: "center" }, props)}> <View {...css({ alignItems: "center" }, props)}>
<Avatar src={avatar} alt={username} placeholder={username} size={UserGrid.layout.size} fill /> <Avatar src={avatar} alt={username} placeholder={username} size={UserGrid.layout.size} fill />
<View {...css({ flexDirection: "row" })}> <View {...css({ flexDirection: "row", alignItems: "center" })}>
<Icon <Icon
icon={!isVerified ? Unverifed : isAdmin ? Admin : UserI} icon={!isVerified ? Unverifed : isAdmin ? Admin : UserI}
{...css({ {...css({
alignSelf: "center",
m: ts(1), m: ts(1),
})} })}
{...tooltip( {...tooltip(
@ -83,9 +81,7 @@ export const UserGrid = ({
), ),
)} )}
/> />
<Skeleton> <P>{username}</P>
<P>{username}</P>
</Skeleton>
<Menu Trigger={IconButton} icon={MoreVert} {...tooltip(t("misc.more"))}> <Menu Trigger={IconButton} icon={MoreVert} {...tooltip(t("misc.more"))}>
{!isVerified && ( {!isVerified && (
<Menu.Item <Menu.Item
@ -159,6 +155,21 @@ export const UserGrid = ({
); );
}; };
UserGrid.Loader = (props: object) => {
const { css } = useYoshiki();
return (
<View {...css({ alignItems: "center" }, props)}>
<Avatar.Loader size={UserGrid.layout.size} />
<View {...css({ flexDirection: "row", alignItems: "center", flexShrink: 1, flexGrow: 1 })}>
<Icon icon={UserI} {...css({ m: ts(1) })} />
<Skeleton {...css({ flexGrow: 1, width: ts(8) })} />
<IconButton icon={MoreVert} disabled />
</View>
</View>
);
};
UserGrid.layout = { UserGrid.layout = {
size: px(150), size: px(150),
numColumns: { xs: 2, sm: 3, md: 5, lg: 6, xl: 7 }, numColumns: { xs: 2, sm: 3, md: 5, lg: 6, xl: 7 },
@ -171,18 +182,20 @@ export const UserList = () => {
return ( return (
<SettingsContainer title={t("admin.users.label")}> <SettingsContainer title={t("admin.users.label")}>
<InfiniteFetch query={UserList.query()} layout={UserGrid.layout}> <InfiniteFetch
{(user) => ( query={UserList.query()}
layout={UserGrid.layout}
Render={({ item }) => (
<UserGrid <UserGrid
isLoading={user.isLoading as any} id={item.id}
id={user.id} username={item.username}
username={user.username} avatar={item.logo}
avatar={user.logo} isAdmin={item.isAdmin}
isAdmin={user.isAdmin} isVerified={item.isVerified}
isVerified={user.isVerified}
/> />
)} )}
</InfiniteFetch> Loader={UserGrid.Loader}
/>
</SettingsContainer> </SettingsContainer>
); );
}; };

View File

@ -155,30 +155,29 @@ export const CollectionPage: QueryPage<{ slug: string }> = ({ slug }) => {
Header={CollectionHeader} Header={CollectionHeader}
headerProps={{ slug }} headerProps={{ slug }}
contentContainerStyle={{ padding: 0, paddingHorizontal: 0, ...pageStyle }} contentContainerStyle={{ padding: 0, paddingHorizontal: 0, ...pageStyle }}
> Render={({ item }) => (
{(x) => (
<ItemDetails <ItemDetails
isLoading={x.isLoading as any} slug={item.slug}
slug={x.slug} type={item.kind}
type={x.kind} name={item.name}
name={x.name} tagline={"tagline" in item ? item.tagline : null}
tagline={"tagline" in x ? x.tagline : null} overview={item.overview}
overview={x.overview} poster={item.poster}
poster={x.poster} subtitle={item.kind !== "collection" ? getDisplayDate(item) : null}
subtitle={x.kind !== "collection" && !x.isLoading ? getDisplayDate(x) : undefined} genres={"genres" in item ? item.genres : null}
genres={"genres" in x ? x.genres : null} href={item.href}
href={x.href} playHref={item.kind !== "collection" ? item.playHref : null}
playHref={x.kind !== "collection" && !x.isLoading ? x.playHref : undefined} watchStatus={(item.kind !== "collection" && item.watchStatus?.status) || null}
watchStatus={
!x.isLoading && x.kind !== "collection" ? x.watchStatus?.status ?? null : null
}
unseenEpisodesCount={ unseenEpisodesCount={
x.kind === "show" ? x.watchStatus?.unseenEpisodesCount ?? x.episodesCount! : null item.kind === "show"
? item.watchStatus?.unseenEpisodesCount ?? item.episodesCount!
: null
} }
{...css({ marginX: ItemGrid.layout.gap })} {...css({ marginX: ItemGrid.layout.gap })}
/> />
)} )}
</InfiniteFetch> Loader={ItemDetails.Loader}
/>
); );
}; };

View File

@ -77,9 +77,9 @@ export const SearchPage: QueryPage<{ q?: string }> = ({ q }) => {
/> />
} }
contentContainerStyle={pageStyle} contentContainerStyle={pageStyle}
> Render={({ item }) => <LayoutComponent {...itemMap(item)} />}
{(item) => <LayoutComponent {...itemMap(item)} />} Loader={LayoutComponent.Loader}
</InfiniteFetch> />
); );
}; };