Skip to content

Commit

Permalink
feat: use skeleton loaders on some important pages
Browse files Browse the repository at this point in the history
  • Loading branch information
sylv committed Feb 11, 2024
1 parent ef18709 commit fc00711
Show file tree
Hide file tree
Showing 12 changed files with 233 additions and 113 deletions.
1 change: 1 addition & 0 deletions packages/web/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default {
overwrite: true,
schema: '../api/src/schema.gql',
documents: ['src/**/*.tsx'],
errorsOnly: true,
generates: {
'src/@generated/': {
preset: 'client',
Expand Down
2 changes: 1 addition & 1 deletion packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"build": "tsc --noEmit && rm -rf ./dist/* && vavite build && tsup && rm -rf ./dist/server",
"generate": "graphql-codegen --config codegen.ts",
"start": "node ./dist/index.js",
"watch": "concurrently \"vavite serve\" \"pnpm generate --watch --errors-only\""
"watch": "concurrently \"vavite serve\" \"pnpm generate --watch\""
},
"devDependencies": {
"@0no-co/graphqlsp": "^1.3.0",
Expand Down
9 changes: 4 additions & 5 deletions packages/web/src/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export enum ButtonStyle {
Disabled = 'bg-dark-300 hover:bg-dark-400 cursor-not-allowed',
}

export const BASE_BUTTON_CLASSES =
'flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium transition rounded truncate max-h-[2.65em]';

export const Button = forwardRef<any, ButtonProps>(
(
{
Expand All @@ -38,11 +41,7 @@ export const Button = forwardRef<any, ButtonProps>(
if (disabled) style = ButtonStyle.Disabled;
const onClickWrap = disabled || loading ? undefined : onClick;
const onKeyDownWrap = disabled || loading ? undefined : onKeyDown;
const classes = clsx(
'flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium transition rounded truncate max-h-[2.65em]',
className,
style,
);
const classes = clsx(BASE_BUTTON_CLASSES, className, style);

return (
<As
Expand Down
10 changes: 4 additions & 6 deletions packages/web/src/components/input/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,7 @@ export function InputContainer<T extends InputChildPropsBase>({
const formik = useContext<FormikContextType<any>>(FormikContext);
const errorMessage = !!(formik && id && formik.touched[id]) && (formik.errors[id] as string);
if (errorMessage) style = InputStyle.Error;
const childClasses = clsx(
'w-full h-full px-3 py-2 rounded outline-none border transition duration-75',
maxHeight && 'max-h-[calc(2.65em-2px)]',
style,
className
);
const childClasses = clsx(BASE_INPUT_CLASSES, maxHeight && BASE_INPUT_MAX_HEIGHT, className, style);

if (formik) {
if (!id) {
Expand Down Expand Up @@ -74,3 +69,6 @@ export function InputContainer<T extends InputChildPropsBase>({
</Fragment>
);
}

export const BASE_INPUT_CLASSES = 'w-full h-full px-3 py-2 rounded outline-none border transition duration-75';
export const BASE_INPUT_MAX_HEIGHT = 'max-h-[calc(2.65em-2px)]';
59 changes: 59 additions & 0 deletions packages/web/src/components/skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import clsx from 'clsx';
import { FC, Fragment, ReactNode, memo } from 'react';
import { BASE_BUTTON_CLASSES } from './button';
import { BASE_INPUT_CLASSES, BASE_INPUT_MAX_HEIGHT } from './input/container';

interface SkeletonProps {
className?: string;
}

export const Skeleton = memo<SkeletonProps>(({ className }) => {
const hasHeight = !className?.includes(' h-');
return <div className={clsx('animate-pulse bg-gray-800 rounded-md', className, hasHeight && 'h-4')} />;
});

interface SkeletonListProps {
count: number;
children: ReactNode;
className?: string;
as?: FC | FC<{ className?: string }>;
}

export const SkeletonList = memo<SkeletonListProps>(({ count, children, className, as }) => {
const Component = as || 'div';
return (
<Component className={className}>
{Array.from({ length: count }).map((_, i) => (
<Fragment key={i}>{children}</Fragment>
))}
</Component>
);
});

interface InputSkeletonProps {
maxHeight?: boolean;
className?: string;
}

export const InputSkeleton = memo<InputSkeletonProps>(({ maxHeight = true, className }) => {
const classes = clsx(BASE_INPUT_CLASSES, maxHeight && BASE_INPUT_MAX_HEIGHT, className, 'border-transparent');
return <Skeleton className={classes} />;
});

export const ButtonSkeleton = memo<SkeletonProps>(({ className }) => {
return <Skeleton className={clsx(BASE_BUTTON_CLASSES, className, 'min-h-[2.65em] min-w-[20em]')} />;
});

export const SkeletonWrap = memo<{
show: boolean;
children: ReactNode;
}>(({ show, children }) => {
if (!show) return children;

return (
<span className="relative">
<Skeleton className="absolute inset-0 h-full" />
<span className="opacity-0">{children}</span>
</span>
);
});
47 changes: 27 additions & 20 deletions packages/web/src/containers/config-generator/config-generator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { FiDownload } from 'react-icons/fi';
import { RegularUserFragment } from '../../@generated/graphql';
import { Container } from '../../components/container';
import { Section } from '../../components/section';
import { Spinner } from '../../components/spinner';
import { Skeleton, SkeletonList, SkeletonWrap } from '../../components/skeleton';
import { Toggle } from '../../components/toggle';
import { downloadFile } from '../../helpers/download.helper';
import { generateConfig } from '../../helpers/generate-config.helper';
import { useConfig } from '../../hooks/useConfig';
import { CustomisationOption } from './customisation-option';

export interface ConfigGeneratorProps {
user: RegularUserFragment & { token: string };
user?: RegularUserFragment & { token: string };
}

export const ConfigGenerator: FC<ConfigGeneratorProps> = ({ user }) => {
Expand All @@ -23,7 +23,7 @@ export const ConfigGenerator: FC<ConfigGeneratorProps> = ({ user }) => {
const downloadable = !!selectedHosts[0];

const download = () => {
if (!downloadable) return;
if (!downloadable || !user) return;
const { name, content } = generateConfig({
direct: !embedded,
hosts: selectedHosts,
Expand All @@ -39,19 +39,16 @@ export const ConfigGenerator: FC<ConfigGeneratorProps> = ({ user }) => {
<Section>
<Container className="flex flex-col justify-between dots selection:bg-purple-600 py-8 px-0">
<div className="w-full flex-grow">
{!config.data && (
<div className="flex items-center justify-center w-full h-full py-10">
<Spinner className="w-auto" />
</div>
)}
{config.data && (
<Fragment>
<Fragment>
<SkeletonWrap show={!config.data}>
<div className="font-bold text-xl">Config Generator</div>
<p className="text-sm text-gray-400">
Pick the hosts you want with the options you think will suit you best. These options are saved in the
config file and are not persisted between sessions. Changing them will not affect existing config files.
</p>
<div className="flex flex-col gap-2 mt-6">
</SkeletonWrap>
<div className="flex flex-col gap-2 mt-6">
<SkeletonWrap show={!config.data}>
<CustomisationOption
title="Direct Links"
description="Embedded links are recommended and will embed the image in the site with additional metadata and functionality like syntax highlighting. Direct links will return links that take you straight to the image, which may have better compatibility with some services."
Expand All @@ -72,6 +69,8 @@ export const ConfigGenerator: FC<ConfigGeneratorProps> = ({ user }) => {
]}
/>
</CustomisationOption>
</SkeletonWrap>
<SkeletonWrap show={!config.data}>
<CustomisationOption
title="Paste Shortcut"
description="Whether to redirect text file uploads to the pastes endpoint"
Expand All @@ -92,10 +91,12 @@ export const ConfigGenerator: FC<ConfigGeneratorProps> = ({ user }) => {
]}
/>
</CustomisationOption>
</div>
<div className="mt-2">
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 mt-2">
{config.data.hosts.map((host) => {
</SkeletonWrap>
</div>
<div className="mt-2">
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 mt-2">
{config.data &&
config.data.hosts.map((host) => {
const isSelected = selectedHosts.includes(host.normalised);
const classes = clsx(
'rounded px-2 py-1 truncate transition border border-transparent',
Expand All @@ -116,14 +117,18 @@ export const ConfigGenerator: FC<ConfigGeneratorProps> = ({ user }) => {
}
}}
>
{host.normalised.replace('{{username}}', user.username)}
{user ? host.normalised.replace('{{username}}', user.username) : host.normalised}
</button>
);
})}
</div>
{!config.data && (
<SkeletonList count={6} as={Fragment}>
<Skeleton className="h-8" />
</SkeletonList>
)}
</div>
</Fragment>
)}
</div>
</Fragment>
</div>
<button
type="submit"
Expand All @@ -133,7 +138,9 @@ export const ConfigGenerator: FC<ConfigGeneratorProps> = ({ user }) => {
downloadable ? 'text-purple-400 hover:underline' : 'text-gray-700 cursor-not-allowed',
)}
>
download config <FiDownload className="h-3.5 w-3.5" />
<SkeletonWrap show={!config.data}>
download config <FiDownload className="h-3.5 w-3.5" />
</SkeletonWrap>
</button>
</Container>
</Section>
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/containers/file-list/cards/file-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FiFileMinus, FiTrash } from 'react-icons/fi';
import { graphql } from '../../../@generated';
import { FileCardFragment } from '../../../@generated/graphql';
import { Link } from '../../../components/link';
import { Skeleton } from '../../../components/skeleton';
import { useConfig } from '../../../hooks/useConfig';
import { MissingPreview } from '../missing-preview';

Expand Down Expand Up @@ -78,3 +79,5 @@ export const FileCard = memo<{ file: FileCardFragment }>(({ file }) => {
</Link>
);
});

export const FileCardSkeleton = () => <Skeleton className="h-44" />;
12 changes: 7 additions & 5 deletions packages/web/src/containers/file-list/file-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ import { graphql } from '../../@generated';
import { Breadcrumbs } from '../../components/breadcrumbs';
import { Card } from '../../components/card';
import { Error } from '../../components/error';
import { PageLoader } from '../../components/page-loader';
import { SkeletonList } from '../../components/skeleton';
import { Toggle } from '../../components/toggle';
import { useQueryState } from '../../hooks/useQueryState';
import { FileCard } from './cards/file-card';
import { FileCard, FileCardSkeleton } from './cards/file-card';
import { PasteCard } from './cards/paste-card';

const PER_PAGE = 24;

const GetFilesQuery = graphql(`
query GetFiles($after: String) {
user {
Expand Down Expand Up @@ -89,7 +87,11 @@ export const FileList: FC = () => {
</div>
</div>
<div className="pb-5">
{!source.data && <PageLoader />}
{!source.data && (
<SkeletonList count={12} className="grid grid-cols-2 gap-4 md:grid-cols-4 lg:grid-cols-6">
<FileCardSkeleton />
</SkeletonList>
)}
{filter === 'files' && (
<div className="grid grid-cols-2 gap-4 md:grid-cols-4 lg:grid-cols-6">
{files.data?.user.files.edges.map(({ node }) => <FileCard key={node.id} file={node} />)}
Expand Down
11 changes: 5 additions & 6 deletions packages/web/src/hooks/useUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,22 @@ export const useLogoutUser = () => {
};

export const useUserRedirect = (
data: RegularUserFragment | null | undefined,
loading: boolean,
query: { data: { user: RegularUserFragment } | null | undefined; loading: boolean; called: boolean },
redirect: boolean | undefined,
) => {
useEffect(() => {
if (!data && !loading && redirect) {
if (!query.data && !query.loading && query.called && redirect) {
navigate(`/login?to=${window.location.href}`);
}
}, [redirect, data, loading]);
}, [redirect, query.data, query.loading, query.called]);
};

export const useUser = <T extends TypedDocumentNode<GetUserQuery, any>>(redirect?: boolean, query?: T) => {
const { login, otpRequired } = useLoginUser();
const { logout } = useLogoutUser();
const { data, loading, error } = useQuery((query || UserQuery) as T);
const { data, loading, called, error } = useQuery((query || UserQuery) as T);

useUserRedirect(data?.user, loading, redirect);
useUserRedirect({ data, loading, called }, redirect);

return {
data: data?.user as RegularUserFragment | null | undefined,
Expand Down
Loading

0 comments on commit fc00711

Please sign in to comment.