mirror of https://github.com/sylv/micro.git
feat: use skeleton loaders on some important pages
This commit is contained in:
parent
ef18709612
commit
fc0071165f
|
@ -4,6 +4,7 @@ export default {
|
|||
overwrite: true,
|
||||
schema: '../api/src/schema.gql',
|
||||
documents: ['src/**/*.tsx'],
|
||||
errorsOnly: true,
|
||||
generates: {
|
||||
'src/@generated/': {
|
||||
preset: 'client',
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>(
|
||||
(
|
||||
{
|
||||
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
@ -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)]';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
|
@ -4,7 +4,7 @@ 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';
|
||||
|
@ -12,7 +12,7 @@ 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 }) => {
|
||||
|
@ -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,
|
||||
|
@ -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."
|
||||
|
@ -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"
|
||||
|
@ -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',
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -78,3 +79,5 @@ export const FileCard = memo<{ file: FileCardFragment }>(({ file }) => {
|
|||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
export const FileCardSkeleton = () => <Skeleton className="h-44" />;
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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} />)}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { FC } from 'react';
|
||||
import { FC, Fragment } from 'react';
|
||||
import { graphql } from '../../../@generated';
|
||||
import { GetUserDocument } from '../../../@generated/graphql';
|
||||
import { Breadcrumbs } from '../../../components/breadcrumbs';
|
||||
|
@ -7,12 +7,11 @@ import { Button } from '../../../components/button';
|
|||
import { Container } from '../../../components/container';
|
||||
import { Input } from '../../../components/input/input';
|
||||
import { OtpInput } from '../../../components/input/otp';
|
||||
import { PageLoader } from '../../../components/page-loader';
|
||||
import { ButtonSkeleton, InputSkeleton, Skeleton } from '../../../components/skeleton';
|
||||
import { Title } from '../../../components/title';
|
||||
import { ConfigGenerator } from '../../../containers/config-generator/config-generator';
|
||||
import { navigate } from '../../../helpers/routing';
|
||||
import { useAsync } from '../../../hooks/useAsync';
|
||||
import { useConfig } from '../../../hooks/useConfig';
|
||||
import { useLogoutUser, useUserRedirect } from '../../../hooks/useUser';
|
||||
|
||||
const RefreshToken = graphql(`
|
||||
|
@ -40,9 +39,8 @@ const UserQueryWithToken = graphql(`
|
|||
`);
|
||||
|
||||
export const Page: FC = () => {
|
||||
const { logout } = useLogoutUser();
|
||||
const user = useQuery(UserQueryWithToken);
|
||||
const config = useConfig();
|
||||
const { logout } = useLogoutUser();
|
||||
const [refreshMutation] = useMutation(RefreshToken);
|
||||
const [refresh, refreshing] = useAsync(async () => {
|
||||
// eslint-disable-next-line no-alert
|
||||
|
@ -52,16 +50,12 @@ export const Page: FC = () => {
|
|||
await logout();
|
||||
});
|
||||
|
||||
useUserRedirect(user.data?.user, user.loading, true);
|
||||
useUserRedirect(user, true);
|
||||
|
||||
const [disableOTP, disableOTPMut] = useMutation(DisableOtp, {
|
||||
refetchQueries: [{ query: GetUserDocument }],
|
||||
});
|
||||
|
||||
if (!user.data || !config.data) {
|
||||
return <PageLoader title="Preferences" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Title>Preferences</Title>
|
||||
|
@ -70,38 +64,66 @@ export const Page: FC = () => {
|
|||
</Breadcrumbs>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="left col-span-full md:col-span-1">
|
||||
<div className="font-bold text-xl">Upload Token</div>
|
||||
<p className="text-sm mt-2 text-gray-400">
|
||||
This token is used when uploading files.{' '}
|
||||
<button type="button" className="text-purple-400 hover:underline" onClick={refresh} disabled={refreshing}>
|
||||
Click here
|
||||
</button>{' '}
|
||||
to reset your token and invalidate all existing ShareX configurations.
|
||||
</p>
|
||||
{user.data && (
|
||||
<Fragment>
|
||||
<div className="font-bold text-xl">Upload Token</div>
|
||||
<p className="text-sm mt-2 text-gray-400">
|
||||
This token is used when uploading files.{' '}
|
||||
<button
|
||||
type="button"
|
||||
className="text-purple-400 hover:underline"
|
||||
onClick={refresh}
|
||||
disabled={refreshing}
|
||||
>
|
||||
Click here
|
||||
</button>{' '}
|
||||
to reset your token and invalidate all existing ShareX configurations.
|
||||
</p>
|
||||
</Fragment>
|
||||
)}
|
||||
{!user.data && (
|
||||
<Fragment>
|
||||
<Skeleton className="w-1/2 mb-1" />
|
||||
<Skeleton className="w-3/4" />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
<div className="right flex items-center col-span-full md:col-span-1">
|
||||
<Input
|
||||
readOnly
|
||||
value={user.data.user.token}
|
||||
onFocus={(event) => {
|
||||
event.target.select();
|
||||
}}
|
||||
/>
|
||||
{user.data && (
|
||||
<Input
|
||||
readOnly
|
||||
value={user.data.user.token}
|
||||
onFocus={(event) => {
|
||||
event.target.select();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!user.data && <InputSkeleton />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
<ConfigGenerator user={user.data.user} />
|
||||
<ConfigGenerator user={user.data?.user} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 mt-8">
|
||||
<div className="left col-span-full md:col-span-1">
|
||||
<div className="font-bold text-xl">2-factor Authentication</div>
|
||||
<p className="text-sm mt-2 text-gray-400">
|
||||
2-factor authentication is currently {user.data.user.otpEnabled ? 'enabled' : 'disabled'}.{' '}
|
||||
{user.data.user.otpEnabled ? `Enter an authenticator code to disable it.` : 'Click to setup.'}
|
||||
</p>
|
||||
{user.data && (
|
||||
<Fragment>
|
||||
<div className="font-bold text-xl">2-factor Authentication</div>
|
||||
<p className="text-sm mt-2 text-gray-400">
|
||||
2-factor authentication is currently {user.data.user.otpEnabled ? 'enabled' : 'disabled'}.{' '}
|
||||
{user.data.user.otpEnabled ? `Enter an authenticator code to disable it.` : 'Click to setup.'}
|
||||
</p>
|
||||
</Fragment>
|
||||
)}
|
||||
{!user.data && (
|
||||
<Fragment>
|
||||
<Skeleton className="w-1/2 mb-1" />
|
||||
<Skeleton className="w-3/4" />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
<div className="right flex items-center col-span-full md:col-span-1">
|
||||
{user.data.user.otpEnabled && (
|
||||
{user.data && user.data.user.otpEnabled && (
|
||||
<OtpInput
|
||||
loading={disableOTPMut.loading}
|
||||
onCode={(otpCode) => {
|
||||
|
@ -111,11 +133,12 @@ export const Page: FC = () => {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{!user.data.user.otpEnabled && (
|
||||
{user.data && !user.data.user.otpEnabled && (
|
||||
<Button className="w-auto ml-auto" onClick={() => navigate(`/dashboard/mfa`)}>
|
||||
Enable 2FA
|
||||
</Button>
|
||||
)}
|
||||
{!user.data && <ButtonSkeleton className="ml-auto" />}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
|
|
@ -2,13 +2,13 @@ import { useMutation, useQuery } from '@apollo/client';
|
|||
import clsx from 'clsx';
|
||||
import copyToClipboard from 'copy-to-clipboard';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { FiDownload, FiShare, FiTrash } from 'react-icons/fi';
|
||||
import { graphql } from '../../../@generated';
|
||||
import { Container } from '../../../components/container';
|
||||
import { Embed } from '../../../components/embed/embed';
|
||||
import { Error } from '../../../components/error';
|
||||
import { PageLoader } from '../../../components/page-loader';
|
||||
import { Skeleton, SkeletonList } from '../../../components/skeleton';
|
||||
import { Spinner } from '../../../components/spinner';
|
||||
import { Title } from '../../../components/title';
|
||||
import { useToasts } from '../../../components/toast';
|
||||
|
@ -114,46 +114,59 @@ export const Page: FC<PageProps> = ({ routeParams }) => {
|
|||
return <Error error={file.error} />;
|
||||
}
|
||||
|
||||
if (!file.data) {
|
||||
return <PageLoader />;
|
||||
}
|
||||
|
||||
const canDelete = file.data.file.isOwner || deleteKey;
|
||||
const canDelete = file.data?.file.isOwner || deleteKey;
|
||||
|
||||
return (
|
||||
<Container className="mt-5 md-2 md:mb-5">
|
||||
<Title>{file.data.file.displayName}</Title>
|
||||
{file.data && <Title>{file.data.file.displayName}</Title>}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-6 pb-1">
|
||||
<div className="flex items-end col-span-5 overflow-hidden whitespace-nowrap pb-1">
|
||||
<h1 className="mr-2 text-xl font-bold md:text-4xl md:break-all">{file.data.file.displayName}</h1>
|
||||
<span className="text-xs text-gray-500">{file.data.file.sizeFormatted}</span>
|
||||
{file.data && (
|
||||
<Fragment>
|
||||
<h1 className="mr-2 text-xl font-bold md:text-4xl md:break-all">{file.data.file.displayName}</h1>
|
||||
<span className="text-xs text-gray-500">{file.data.file.sizeFormatted}</span>
|
||||
</Fragment>
|
||||
)}
|
||||
{!file.data && <Skeleton className="w-1/2 h-8" />}
|
||||
</div>
|
||||
<div className="col-span-5">
|
||||
<Embed
|
||||
data={{
|
||||
type: file.data.file.type,
|
||||
paths: file.data.file.paths,
|
||||
size: file.data.file.size,
|
||||
displayName: file.data.file.displayName,
|
||||
height: file.data.file.metadata?.height,
|
||||
width: file.data.file.metadata?.width,
|
||||
textContent: file.data.file.textContent,
|
||||
}}
|
||||
/>
|
||||
{file.data && (
|
||||
<Embed
|
||||
data={{
|
||||
type: file.data.file.type,
|
||||
paths: file.data.file.paths,
|
||||
size: file.data.file.size,
|
||||
displayName: file.data.file.displayName,
|
||||
height: file.data.file.metadata?.height,
|
||||
width: file.data.file.metadata?.width,
|
||||
textContent: file.data.file.textContent,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!file.data && <Skeleton className="w-full h-[50dvh]" />}
|
||||
</div>
|
||||
<div className="flex md:flex-col">
|
||||
<div className="flex text-sm gap-3 text-gray-500 cursor-pointer md:flex-col">
|
||||
<FileOption onClick={copyLink}>
|
||||
<FiShare className="h-4 mr-1" /> Copy link
|
||||
</FileOption>
|
||||
<FileOption onClick={downloadFile}>
|
||||
<FiDownload className="h-4 mr-1" /> Download
|
||||
</FileOption>
|
||||
{canDelete && (
|
||||
<FileOption onClick={deleteFile} className="text-red-400 hover:text-red-500">
|
||||
<FiTrash className="h-4 mr-1" />
|
||||
{deletingFile ? <Spinner size="small" /> : confirm ? 'Are you sure?' : 'Delete'}
|
||||
</FileOption>
|
||||
{file.data && (
|
||||
<Fragment>
|
||||
<FileOption onClick={copyLink}>
|
||||
<FiShare className="h-4 mr-1" /> Copy link
|
||||
</FileOption>
|
||||
<FileOption onClick={downloadFile}>
|
||||
<FiDownload className="h-4 mr-1" /> Download
|
||||
</FileOption>
|
||||
{canDelete && (
|
||||
<FileOption onClick={deleteFile} className="text-red-400 hover:text-red-500">
|
||||
<FiTrash className="h-4 mr-1" />
|
||||
{deletingFile ? <Spinner size="small" /> : confirm ? 'Are you sure?' : 'Delete'}
|
||||
</FileOption>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
{!file.data && (
|
||||
<SkeletonList count={3} className="space-y-2">
|
||||
<Skeleton className="w-1/2" />
|
||||
</SkeletonList>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1434,7 +1434,7 @@ packages:
|
|||
'@fastify/reply-from': 9.7.0
|
||||
fast-querystring: 1.1.2
|
||||
fastify-plugin: 4.5.1
|
||||
ws: 8.16.0(utf-8-validate@6.0.3)
|
||||
ws: 8.16.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
@ -1836,7 +1836,7 @@ packages:
|
|||
graphql-ws: 5.14.3(graphql@16.8.1)
|
||||
isomorphic-ws: 5.0.0(ws@8.16.0)
|
||||
tslib: 2.6.2
|
||||
ws: 8.16.0(utf-8-validate@6.0.3)
|
||||
ws: 8.16.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
@ -1871,7 +1871,7 @@ packages:
|
|||
graphql: 16.8.1
|
||||
isomorphic-ws: 5.0.0(ws@8.16.0)
|
||||
tslib: 2.6.2
|
||||
ws: 8.16.0(utf-8-validate@6.0.3)
|
||||
ws: 8.16.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
@ -2123,7 +2123,7 @@ packages:
|
|||
isomorphic-ws: 5.0.0(ws@8.16.0)
|
||||
tslib: 2.6.2
|
||||
value-or-promise: 1.0.12
|
||||
ws: 8.16.0(utf-8-validate@6.0.3)
|
||||
ws: 8.16.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- bufferutil
|
||||
|
@ -7333,7 +7333,7 @@ packages:
|
|||
peerDependencies:
|
||||
ws: '*'
|
||||
dependencies:
|
||||
ws: 8.16.0(utf-8-validate@6.0.3)
|
||||
ws: 8.16.0
|
||||
dev: true
|
||||
|
||||
/istextorbinary@9.5.0:
|
||||
|
@ -8476,6 +8476,7 @@ packages:
|
|||
/node-gyp-build@4.8.0:
|
||||
resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/node-html-parser@6.1.12:
|
||||
resolution: {integrity: sha512-/bT/Ncmv+fbMGX96XG9g05vFt43m/+SYKIs9oAemQVYyVcZmDAI2Xq/SbNcpOA35eF0Zk2av3Ksf+Xk8Vt8abA==}
|
||||
|
@ -10952,6 +10953,7 @@ packages:
|
|||
requiresBuild: true
|
||||
dependencies:
|
||||
node-gyp-build: 4.8.0
|
||||
dev: false
|
||||
|
||||
/util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
@ -11353,6 +11355,19 @@ packages:
|
|||
utf-8-validate: 6.0.3
|
||||
dev: false
|
||||
|
||||
/ws@8.16.0:
|
||||
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
dev: true
|
||||
|
||||
/ws@8.16.0(utf-8-validate@6.0.3):
|
||||
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
@ -11366,6 +11381,7 @@ packages:
|
|||
optional: true
|
||||
dependencies:
|
||||
utf-8-validate: 6.0.3
|
||||
dev: false
|
||||
|
||||
/xtend@4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
|
|
Loading…
Reference in New Issue