client: add some loading skeletons
This commit is contained in:
parent
cad10fb1fa
commit
cb6faa50b0
11 changed files with 113 additions and 55 deletions
|
@ -13,6 +13,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fileNameContainer {
|
.fileNameContainer {
|
||||||
|
|
|
@ -4,17 +4,19 @@ import styles from './document.module.css'
|
||||||
import MarkdownPreview from '../preview'
|
import MarkdownPreview from '../preview'
|
||||||
import { Trash } from '@geist-ui/icons'
|
import { Trash } from '@geist-ui/icons'
|
||||||
import FormattingIcons from "../formatting-icons"
|
import FormattingIcons from "../formatting-icons"
|
||||||
|
import Skeleton from "react-loading-skeleton"
|
||||||
type Props = {
|
type Props = {
|
||||||
editable: boolean
|
editable?: boolean
|
||||||
remove?: () => void
|
remove?: () => void
|
||||||
title?: string
|
title?: string
|
||||||
content?: string
|
content?: string
|
||||||
setTitle?: (title: string) => void
|
setTitle?: (title: string) => void
|
||||||
setContent?: (content: string) => void
|
setContent?: (content: string) => void
|
||||||
initialTab?: "edit" | "preview"
|
initialTab?: "edit" | "preview"
|
||||||
|
skeleton?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Document = ({ remove, editable, title, content, setTitle, setContent, initialTab = 'edit' }: Props) => {
|
const Document = ({ remove, editable, title, content, setTitle, setContent, initialTab = 'edit', skeleton }: Props) => {
|
||||||
const codeEditorRef = useRef<HTMLTextAreaElement>(null)
|
const codeEditorRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const [tab, setTab] = useState(initialTab)
|
const [tab, setTab] = useState(initialTab)
|
||||||
const height = editable ? "500px" : '100%'
|
const height = editable ? "500px" : '100%'
|
||||||
|
@ -46,7 +48,21 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (skeleton) {
|
||||||
|
return <>
|
||||||
|
<Spacer height={1} />
|
||||||
|
<Card marginBottom={'var(--gap)'} marginTop={'var(--gap)'} style={{ maxWidth: 980, margin: "0 auto" }}>
|
||||||
|
<div className={styles.fileNameContainer}>
|
||||||
|
<Skeleton width={275} height={36} />
|
||||||
|
{editable && <Skeleton width={36} height={36} />}
|
||||||
|
</div>
|
||||||
|
<div className={styles.descriptionContainer}>
|
||||||
|
<div style={{ flexDirection: 'row', display: 'flex' }}><Skeleton width={125} height={36} /></div>
|
||||||
|
<Skeleton width={'100%'} height={350} />
|
||||||
|
</div >
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Spacer height={1} />
|
<Spacer height={1} />
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { Loading, Card, Divider, Input, Text, Grid, Spacer } from "@geist-ui/core"
|
import { Text } from "@geist-ui/core"
|
||||||
import Preview from "../preview"
|
|
||||||
import ShiftBy from "../shift-by"
|
|
||||||
import VisibilityBadge from "../visibility-badge"
|
|
||||||
import Link from '../Link'
|
import Link from '../Link'
|
||||||
|
|
||||||
import styles from './post-list.module.css'
|
import styles from './post-list.module.css'
|
||||||
|
import ListItemSkeleton from "./list-item-skeleton"
|
||||||
|
import ListItem from "./list-item"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
posts: any
|
posts: any
|
||||||
|
@ -12,49 +11,23 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const PostList = ({ posts, error }: Props) => {
|
const PostList = ({ posts, error }: Props) => {
|
||||||
const FilenameInput = ({ title }: { title: string }) => <Input
|
|
||||||
value={title}
|
|
||||||
marginTop="var(--gap-double)"
|
|
||||||
size={1.2}
|
|
||||||
font={1.2}
|
|
||||||
label="Filename"
|
|
||||||
readOnly
|
|
||||||
width={"100%"}
|
|
||||||
/>
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{error && <Text type='error'>Failed to load.</Text>}
|
{error && <Text type='error'>Failed to load.</Text>}
|
||||||
{!posts && <Loading />}
|
{!posts && <ul>
|
||||||
|
<li>
|
||||||
|
<ListItemSkeleton />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ListItemSkeleton />
|
||||||
|
</li>
|
||||||
|
</ul>}
|
||||||
{posts?.length === 0 && <Text>You have no posts. Create one <Link href="/new">here</Link>.</Text>}
|
{posts?.length === 0 && <Text>You have no posts. Create one <Link href="/new">here</Link>.</Text>}
|
||||||
{
|
{
|
||||||
posts?.length > 0 && <div>
|
posts?.length > 0 && <div>
|
||||||
<ul>
|
<ul>
|
||||||
{posts.map((post: any) => {
|
{posts.map((post: any) => {
|
||||||
return <li key={post.id}>
|
return <ListItem post={post} key={post.id} />
|
||||||
<Card style={{ overflowY: 'scroll' }}>
|
|
||||||
<Spacer height={1 / 2} />
|
|
||||||
<Grid.Container justify={'space-between'}>
|
|
||||||
<Grid xs={8}>
|
|
||||||
<Text h3 paddingLeft={1 / 2}>
|
|
||||||
<Link color href={`/post/${post.id}`}>{post.title}
|
|
||||||
<ShiftBy y={-1}><VisibilityBadge visibility={post.visibility} /></ShiftBy>
|
|
||||||
</Link>
|
|
||||||
</Text></Grid>
|
|
||||||
<Grid xs={7}><Text type="secondary" h5>{new Date(post.createdAt).toLocaleDateString()} </Text></Grid>
|
|
||||||
<Grid xs={4}><Text type="secondary" h5>{post.files.length === 1 ? "1 file" : `${post.files.length} files`}</Text></Grid>
|
|
||||||
</Grid.Container>
|
|
||||||
|
|
||||||
<Divider h="1px" my={0} />
|
|
||||||
|
|
||||||
<Card.Content >
|
|
||||||
{post.files.map((file: any) => {
|
|
||||||
return <FilenameInput key={file.id} title={file.title} />
|
|
||||||
})}
|
|
||||||
</Card.Content>
|
|
||||||
|
|
||||||
</Card>
|
|
||||||
</li>
|
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -63,4 +36,4 @@ const PostList = ({ posts, error }: Props) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PostList
|
export default PostList
|
||||||
|
|
19
client/components/post-list/list-item-skeleton.tsx
Normal file
19
client/components/post-list/list-item-skeleton.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Card, Spacer, Grid, Divider } from "@geist-ui/core";
|
||||||
|
import Skeleton from "react-loading-skeleton";
|
||||||
|
|
||||||
|
const ListItemSkeleton = () => (<Card>
|
||||||
|
<Spacer height={1 / 2} />
|
||||||
|
<Grid.Container justify={'space-between'} marginBottom={1 / 2}>
|
||||||
|
<Grid xs={8} paddingLeft={1 / 2}><Skeleton width={150} /></Grid>
|
||||||
|
<Grid xs={7}><Skeleton width={100} /></Grid>
|
||||||
|
<Grid xs={4}><Skeleton width={70} /></Grid>
|
||||||
|
</Grid.Container>
|
||||||
|
|
||||||
|
<Divider h="1px" my={0} />
|
||||||
|
|
||||||
|
<Card.Content >
|
||||||
|
<Skeleton width={200} />
|
||||||
|
</Card.Content>
|
||||||
|
</Card>)
|
||||||
|
|
||||||
|
export default ListItemSkeleton
|
43
client/components/post-list/list-item.tsx
Normal file
43
client/components/post-list/list-item.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { Card, Spacer, Grid, Divider, Link, Text, Input } from "@geist-ui/core"
|
||||||
|
import post from "../post"
|
||||||
|
import ShiftBy from "../shift-by"
|
||||||
|
import VisibilityBadge from "../visibility-badge"
|
||||||
|
|
||||||
|
const FilenameInput = ({ title }: { title: string }) => <Input
|
||||||
|
value={title}
|
||||||
|
marginTop="var(--gap-double)"
|
||||||
|
size={1.2}
|
||||||
|
font={1.2}
|
||||||
|
label="Filename"
|
||||||
|
readOnly
|
||||||
|
width={"100%"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
const ListItem = ({ post }: { post: any }) => {
|
||||||
|
return (<li key={post.id}>
|
||||||
|
<Card style={{ overflowY: 'scroll' }}>
|
||||||
|
<Spacer height={1 / 2} />
|
||||||
|
<Grid.Container justify={'space-between'}>
|
||||||
|
<Grid xs={8}>
|
||||||
|
<Text h3 paddingLeft={1 / 2}>
|
||||||
|
<Link color href={`/post/${post.id}`}>{post.title}
|
||||||
|
<ShiftBy y={-1}><VisibilityBadge visibility={post.visibility} /></ShiftBy>
|
||||||
|
</Link>
|
||||||
|
</Text></Grid>
|
||||||
|
<Grid xs={7}><Text type="secondary" h5>{new Date(post.createdAt).toLocaleDateString()} </Text></Grid>
|
||||||
|
<Grid xs={4}><Text type="secondary" h5>{post.files.length === 1 ? "1 file" : `${post.files.length} files`}</Text></Grid>
|
||||||
|
</Grid.Container>
|
||||||
|
|
||||||
|
<Divider h="1px" my={0} />
|
||||||
|
|
||||||
|
<Card.Content >
|
||||||
|
{post.files.map((file: any) => {
|
||||||
|
return <FilenameInput key={file.id} title={file.title} />
|
||||||
|
})}
|
||||||
|
</Card.Content>
|
||||||
|
|
||||||
|
</Card>
|
||||||
|
</li>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListItem
|
|
@ -23,7 +23,7 @@ const ReactMarkdownPreview = ({ content, height }: Props) => {
|
||||||
return !inline && match ? (
|
return !inline && match ? (
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
lineNumberStyle={{
|
lineNumberStyle={{
|
||||||
minWidth: "1.25rem"
|
minWidth: "2.25rem"
|
||||||
}}
|
}}
|
||||||
customStyle={{
|
customStyle={{
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-debounce-render": "^8.0.2",
|
"react-debounce-render": "^8.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
|
"react-loading-skeleton": "^3.0.3",
|
||||||
"react-markdown": "^8.0.0",
|
"react-markdown": "^8.0.0",
|
||||||
"react-syntax-highlighter": "^15.4.5",
|
"react-syntax-highlighter": "^15.4.5",
|
||||||
"react-syntax-highlighter-virtualized-renderer": "^1.1.0",
|
"react-syntax-highlighter-virtualized-renderer": "^1.1.0",
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { useEffect, useState } from 'react'
|
||||||
import type { AppProps as NextAppProps } from "next/app";
|
import type { AppProps as NextAppProps } from "next/app";
|
||||||
import useSharedState from '../lib/hooks/use-shared-state';
|
import useSharedState from '../lib/hooks/use-shared-state';
|
||||||
|
|
||||||
|
import 'react-loading-skeleton/dist/skeleton.css'
|
||||||
|
|
||||||
export type ThemeProps = {
|
export type ThemeProps = {
|
||||||
theme: "light" | "dark" | string,
|
theme: "light" | "dark" | string,
|
||||||
changeTheme: () => void
|
changeTheme: () => void
|
||||||
|
|
|
@ -3,13 +3,9 @@ import styles from '../styles/Home.module.css'
|
||||||
import { Page } from '@geist-ui/core'
|
import { Page } from '@geist-ui/core'
|
||||||
|
|
||||||
import Header from '../components/header'
|
import Header from '../components/header'
|
||||||
import useSignedIn from '../lib/hooks/use-signed-in'
|
|
||||||
import { Loader } from '@geist-ui/icons'
|
|
||||||
import MyPosts from '../components/my-posts'
|
import MyPosts from '../components/my-posts'
|
||||||
|
|
||||||
const Home = ({ theme, changeTheme }: { theme: "light" | "dark", changeTheme: () => void }) => {
|
const Home = ({ theme, changeTheme }: { theme: "light" | "dark", changeTheme: () => void }) => {
|
||||||
const { isLoading, isSignedIn } = useSignedIn({ redirectIfNotAuthed: true })
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page className={styles.container} width="100%">
|
<Page className={styles.container} width="100%">
|
||||||
<Head>
|
<Head>
|
||||||
|
@ -21,8 +17,7 @@ const Home = ({ theme, changeTheme }: { theme: "light" | "dark", changeTheme: ()
|
||||||
<Header theme={theme} changeTheme={changeTheme} />
|
<Header theme={theme} changeTheme={changeTheme} />
|
||||||
</Page.Header>
|
</Page.Header>
|
||||||
<Page.Content paddingTop={"var(--gap)"} width={"var(--main-content-width)"} margin="0 auto" className={styles.main}>
|
<Page.Content paddingTop={"var(--gap)"} width={"var(--main-content-width)"} margin="0 auto" className={styles.main}>
|
||||||
{isLoading && <div style={{ margin: "0 auto" }}><Loader /></div>}
|
<MyPosts />
|
||||||
{isSignedIn && <MyPosts />}
|
|
||||||
</Page.Content>
|
</Page.Content>
|
||||||
</Page >
|
</Page >
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { Loading, Page, Text } from "@geist-ui/core";
|
import { Page, Text } from "@geist-ui/core";
|
||||||
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Document from '../../components/document'
|
import Document from '../../components/document'
|
||||||
import Header from "../../components/header";
|
import Header from "../../components/header";
|
||||||
import VisibilityBadge from "../../components/visibility-badge";
|
import VisibilityBadge from "../../components/visibility-badge";
|
||||||
|
@ -51,7 +52,10 @@ const Post = ({ theme, changeTheme }: ThemeProps) => {
|
||||||
</Page.Header>
|
</Page.Header>
|
||||||
<Page.Content width={"var(--main-content-width)"} margin="auto">
|
<Page.Content width={"var(--main-content-width)"} margin="auto">
|
||||||
{error && <Text type="error">{error}</Text>}
|
{error && <Text type="error">{error}</Text>}
|
||||||
{!error && (isLoading || !post?.files) && <Loading />}
|
{/* {!error && (isLoading || !post?.files) && <Loading />} */}
|
||||||
|
{!error && isLoading && <><Text h2><Skeleton width={400} /></Text>
|
||||||
|
<Document skeleton={true} />
|
||||||
|
</>}
|
||||||
{!isLoading && post && <><Text h2>{post.title} <VisibilityBadge visibility={post.visibility} /></Text>
|
{!isLoading && post && <><Text h2>{post.title} <VisibilityBadge visibility={post.visibility} /></Text>
|
||||||
{post.files.map(({ id, content, title }: { id: any, content: string, title: string }) => (
|
{post.files.map(({ id, content, title }: { id: any, content: string, title: string }) => (
|
||||||
<Document
|
<Document
|
||||||
|
@ -62,8 +66,7 @@ const Post = ({ theme, changeTheme }: ThemeProps) => {
|
||||||
initialTab={'preview'}
|
initialTab={'preview'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>}
|
||||||
}
|
|
||||||
</Page.Content>
|
</Page.Content>
|
||||||
|
|
||||||
</Page >
|
</Page >
|
||||||
|
|
|
@ -2268,6 +2268,11 @@ react-lifecycles-compat@^3.0.4:
|
||||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||||
|
|
||||||
|
react-loading-skeleton@^3.0.3:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-loading-skeleton/-/react-loading-skeleton-3.0.3.tgz#4f45467cf3193fb25456bc02aeb81922b82b8f1f"
|
||||||
|
integrity sha512-HPkEqQGwmbg1ImcYA9n4hDiLC3u92xUdU+sSfrv/9l3lNBChAubcl1azjV2WakapdkbA3gko+hPzZkZGIb/9xA==
|
||||||
|
|
||||||
react-markdown@^8.0.0:
|
react-markdown@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.0.tgz#3243296a59ddb0f451d262cc2e11123674b416c2"
|
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.0.tgz#3243296a59ddb0f451d262cc2e11123674b416c2"
|
||||||
|
|
Loading…
Reference in a new issue