Skip to content

Commit

Permalink
profile screen skeleton loader (#2438)
Browse files Browse the repository at this point in the history
* add profile skeleton loader

* fix navbar jump bug

* add multi shimmer skeleton

* fix prettier unused field

* fix prettier

* improve multishimmer context and mobile skeleton
  • Loading branch information
pvicensSpacedev authored Apr 26, 2024
1 parent 4efa9d6 commit f77743d
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 49 deletions.
11 changes: 10 additions & 1 deletion apps/web/pages/[username]/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { GetServerSideProps } from 'next';
import { route } from 'nextjs-routes';
import { PropsWithChildren } from 'react';
import { PropsWithChildren, Suspense } from 'react';
import { loadQuery, PreloadedQuery, usePreloadedQuery } from 'react-relay';
import { graphql } from 'relay-runtime';
import styled from 'styled-components';

import breakpoints, { pageGutter } from '~/components/core/breakpoints';
import useVerifyEmailOnPage from '~/components/Email/useVerifyEmailOnPage';
import { ProfileScreenLoadingSkeleton } from '~/components/Profile/ProfileScreenLoadingSkeleton';
import { GalleryNavbar } from '~/contexts/globalLayout/GlobalNavbar/GalleryNavbar/GalleryNavbar';
import { useGlobalNavbarHeight } from '~/contexts/globalLayout/GlobalNavbar/useGlobalNavbarHeight';
import { StandardSidebar } from '~/contexts/globalLayout/GlobalSidebar/StandardSidebar';
Expand Down Expand Up @@ -85,6 +86,14 @@ type UserGalleryProps = MetaTagProps & {
};

export default function UserGallery({ username, preloadedQuery }: UserGalleryProps) {
return (
<Suspense key={username} fallback={<ProfileScreenLoadingSkeleton />}>
<UserGalleryInner username={username} preloadedQuery={preloadedQuery} />
</Suspense>
);
}

function UserGalleryInner({ username, preloadedQuery }: UserGalleryProps) {
const query = usePreloadedQuery<UsernameQuery>(UsernameQueryNode, preloadedQuery);

useVerifyEmailOnPage(query);
Expand Down
42 changes: 42 additions & 0 deletions apps/web/src/components/MultiShimmer/MultiShimmer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import Skeleton from 'react-loading-skeleton';
import styled from 'styled-components';

import { size } from '../core/breakpoints';

const Wrapper = styled.div`
margin-bottom: 60px;
width: 100%;
`;

const ArtworksGridSkeleton = styled.div`
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 16px;
margin-top: 16px;
@media (max-width: ${size.tablet}px) {
// Adjusts to a mobile viewport
grid-template-columns: repeat(1, 1fr);
}
`;

const ArtworkGridItemSkeleton = styled(Skeleton)`
height: 300px;
`;

const SkeletoState = () => {
return (
<Wrapper>
<ArtworksGridSkeleton>
{[...Array(12)].map((_, index) => (
<ArtworkGridItemSkeleton key={index} />
))}
</ArtworksGridSkeleton>
</Wrapper>
);
};

export function MultiShimmer() {
return <SkeletoState />;
}
87 changes: 87 additions & 0 deletions apps/web/src/components/Profile/ProfileScreenLoadingSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react';
import Skeleton from 'react-loading-skeleton';
import styled from 'styled-components';

import { GalleryPageSpacing } from '~/pages/[username]';

import { size } from '../core/breakpoints';
import { VStack } from '../core/Spacer/Stack';

const GallerySkeletonWrapper = styled.div`
display: flex;
flex-direction: column;
`;

const Wrapper = styled.div`
padding-left: 20px;
padding-right: 20px;
margin-bottom: 60px;
`;

const TitleSkeleton = styled(Skeleton)`
margin-top: 20px;
width: 15vw;
height: 2vh;
`;

const UsernameSkeleton = styled(Skeleton)`
margin-top: 10px;
width: 15vw;
height: 2vh;
`;

const UsernameFollowsSkeleton = styled(Skeleton)`
margin-top: 18px;
width: 20vw;
height: 2vh;
`;

const UsernameSocialsSkeleton = styled(Skeleton)`
margin-top: 10px;
width: 10vw;
height: 2vh;
`;

const ArtworksGridSkeleton = styled.div`
display: grid;
grid-template-columns: repeat(4, 1fr); // default to 4 items per row
grid-gap: 16px;
margin-top: 16px;
@media (max-width: ${size.tablet}px) {
// Adjusts to a mobile viewport
grid-template-columns: repeat(1, 1fr); // 1 item per row on mobile
}
`;

const ArtworkGridItemSkeleton = styled(Skeleton)`
height: 300px;
`;

const UserGallerySkeleton = () => {
return (
<GalleryPageSpacing>
<VStack>
<GallerySkeletonWrapper>
<Wrapper>
<UsernameSkeleton />
<UsernameFollowsSkeleton />
<UsernameSocialsSkeleton />
</Wrapper>
</GallerySkeletonWrapper>
</VStack>
<Wrapper>
<TitleSkeleton />
<ArtworksGridSkeleton>
{[...Array(12)].map((_, index) => (
<ArtworkGridItemSkeleton key={index} />
))}
</ArtworksGridSkeleton>
</Wrapper>
</GalleryPageSpacing>
);
};

export function ProfileScreenLoadingSkeleton() {
return <UserGallerySkeleton />;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';

const INITIAL_HEIGHT = 64;
const INITIAL_HEIGHT = 56;

export function useGlobalNavbarHeight() {
const [height, setHeight] = useState(INITIAL_HEIGHT);
Expand Down
89 changes: 84 additions & 5 deletions apps/web/src/contexts/shimmer/ShimmerContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { createContext, memo, ReactNode, useContext, useMemo, useState } from 'react';
import {
createContext,
memo,
PropsWithChildren,
ReactNode,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import styled from 'styled-components';

import { MultiShimmer } from '~/components/MultiShimmer/MultiShimmer';
import Shimmer from '~/components/Shimmer/Shimmer';

type AspectRatio = 'wide' | 'square' | 'tall' | 'unknown';
Expand All @@ -23,7 +33,7 @@ export const useContentState = (): ShimmerState => {

// TODO(Terence): Fix this later
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ContentIsLoadedEvent = (event?: any) => void;
export type ContentIsLoadedEvent = (event?: any, tokenId?: string) => void;

type ShimmerAction = {
setContentIsLoaded: ContentIsLoadedEvent;
Expand All @@ -40,13 +50,67 @@ export const useSetContentIsLoaded = (): ShimmerAction['setContentIsLoaded'] =>
return context.setContentIsLoaded;
};

const MultiShimmerContext = createContext<
{ markTokenAsLoaded: (tokenId: string) => void } | undefined
>(undefined);

export const useMultiShimmerProvider = () => {
const context = useContext(MultiShimmerContext);
if (!context) {
return null;
}
return context;
};

/*
MultiShimmerProvider is used to show a skeleton loader for group of tokens and stop showing it
when X tokens were done loaded. For example, in profile screen we'll show it while the first 12
tokens are loading, after they're finished loading we'll show the actual tokens.
*/

type MultiShimmerProviderProps = PropsWithChildren<{
tokenIdsToLoad: string[];
}>;

export const MultiShimmerProvider = ({ children, tokenIdsToLoad }: MultiShimmerProviderProps) => {
const [waitingForTokenIds, setWaitingForTokenIds] = useState(tokenIdsToLoad);

const markTokenAsLoaded = useCallback((tokenId: string) => {
setWaitingForTokenIds((prev) => prev.filter((id) => id !== tokenId));
}, []);

const value = useMemo(
() => ({
markTokenAsLoaded,
}),
[markTokenAsLoaded]
);

const isLoading = Boolean(waitingForTokenIds.length);

return (
<MultiShimmerContext.Provider value={value}>
{isLoading && (
<Container overflowHidden={isLoading}>
<VisibleDiv isVisible={isLoading}>
<MultiShimmer />
</VisibleDiv>
</Container>
)}
{children}
</MultiShimmerContext.Provider>
);
};

type Props = { children: ReactNode | ReactNode[] };

const ShimmerProvider = memo(({ children }: Props) => {
const [isLoaded, setIsLoaded] = useState(false);
const [aspectRatio, setAspectRatio] = useState<null | number>(null);
const [aspectRatioType, setAspectRatioType] = useState<null | AspectRatio>(null);

const multiShimmerContext = useMultiShimmerProvider();

const state = useMemo(
() => ({
aspectRatio,
Expand All @@ -61,7 +125,7 @@ const ShimmerProvider = memo(({ children }: Props) => {
// such as images, videos, and iframes, each of which come with different properties.
// This is getting fixed in a followup PR
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setContentIsLoaded: (event?: any) => {
setContentIsLoaded: (event?: any, tokenId?: string) => {
if (event) {
// default aspect ratio to 1; if we can't determine an asset's dimensions, we'll show it in a square viewport
let aspectRatio = 1;
Expand Down Expand Up @@ -89,11 +153,13 @@ const ShimmerProvider = memo(({ children }: Props) => {
setAspectRatioType('unknown');
}
}

if (tokenId) {
multiShimmerContext?.markTokenAsLoaded(tokenId);
}
setIsLoaded(true);
},
}),
[]
[multiShimmerContext]
);

return (
Expand Down Expand Up @@ -146,4 +212,17 @@ const StyledChildren = styled.div<VisibleProps>`
opacity: ${({ visible }) => (visible ? 1 : 0)};
`;

type VisibleDivProps = {
isVisible: boolean;
};

const VisibleDiv = styled.div<VisibleDivProps>`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
visibility: ${({ isVisible }) => (isVisible ? 'visible' : 'hidden')};
`;

export default ShimmerProvider;
2 changes: 1 addition & 1 deletion apps/web/src/hooks/useNftRetry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function useNftRetry({ tokenId }: useNftRetryArgs): useNftRetryResult {

const handleNftLoaded = useCallback<ContentIsLoadedEvent>(
(event) => {
shimmerContext?.setContentIsLoaded(event);
shimmerContext?.setContentIsLoaded(event, tokenId);

markTokenAsLoaded(tokenId);
},
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/scenes/GalleryPage/GalleriesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export default function GalleriesPage({ queryRef }: Props) {
galleries {
dbid
id
position @required(action: NONE)
...GalleryFragment
}
}
Expand Down
Loading

0 comments on commit f77743d

Please sign in to comment.