diff --git a/src/scss/_variables.scss b/src/scss/_variables.scss index 92d3d6a..a11ad13 100644 --- a/src/scss/_variables.scss +++ b/src/scss/_variables.scss @@ -37,3 +37,13 @@ /* MISC */ /* -------------------------------------------------------------------------- */ $krist-value-alt-color: $text-muted; // TODO: maybe green? + +$skeleton-color: $lighter; +$skeleton-height: 0.75em; +$skeleton-max-width: 10em; +$skeleton-pulse-opacity: 0.6; +$skeleton-pulse-duration: 1.5s; +$skeleton-pulse-delay: 1s; + +$table-loading-gradient-height: 10rem; +$table-loading-gradient-color: $body-bg; diff --git a/src/shared-components/list-view/ColumnHeader.tsx b/src/shared-components/list-view/ColumnHeader.tsx index 5b8deca..93a4689 100644 --- a/src/shared-components/list-view/ColumnHeader.tsx +++ b/src/shared-components/list-view/ColumnHeader.tsx @@ -33,13 +33,15 @@ // Decide which sort button to render const sortButton = () => { + const sort = () => this.onClickSort(); + switch (sortDirection) { case SortDirection.ASC: - return this.onClickSort()} className="column-sort column-sort-asc icon-up-open" />; + return ; case SortDirection.DESC: - return this.onClickSort()} className="column-sort column-sort-desc icon-down-open" />; + return ; default: // no sort - return this.onClickSort()} className="column-sort column-sort-none icon-down-open" />; + return ; } }; diff --git a/src/shared-components/list-view/ListTable.scss b/src/shared-components/list-view/ListTable.scss index a54ba37..d73688d 100644 --- a/src/shared-components/list-view/ListTable.scss +++ b/src/shared-components/list-view/ListTable.scss @@ -1,7 +1,14 @@ @import "~scss/variables"; .list-view table { + margin-bottom: 0; + thead { border-bottom: 1px solid $border-color; } + + &.loading { + // Fade out the skeleton screen when data is loading + mask-image: linear-gradient(to bottom, black 50%, transparent); + } } diff --git a/src/shared-components/list-view/ListTable.tsx b/src/shared-components/list-view/ListTable.tsx index e646944..2ec587b 100644 --- a/src/shared-components/list-view/ListTable.tsx +++ b/src/shared-components/list-view/ListTable.tsx @@ -7,10 +7,17 @@ import { KristValue } from "@components/krist-value"; import "./ListTable.scss"; +import { SkeletonText } from "@components/skeleton/SkeletonText"; + +// Default widths for the skeleton elements that look good enough +const SKELETON_EM_WIDTHS = [10, 7, 5, 4, 8, 10]; +const SKELETON_ROWS = 5; interface State { orderBy?: Extract; order?: SortDirection; + + loading: boolean; } interface Props { @@ -23,21 +30,66 @@ this.state = { orderBy: undefined, - order: undefined + order: undefined, + + loading: true }; } + componentDidMount(): void { + // TODO: temporary + setTimeout(() => { + this.setState({ loading: false }); + }, 6000); + } + setSort(orderBy?: Extract, order?: SortDirection): void { this.setState({ orderBy, order }); } + generateSkeletonSizes(): Array | undefined { + const { headers } = this.props; + const { loading } = this.state; + + // Generate the sizes of skeleton text + const skeletonSizes = headers && loading + ? Array.from(headers, ([, headerSpec]) => + headerSpec.skeletonEmWidth || undefined) // If no width was defined, use undefined + : undefined; + + // Fill any missing sizes with defaults + if (skeletonSizes !== undefined) { + for (let i = 0; i < Math.min(skeletonSizes.length, SKELETON_EM_WIDTHS.length); i++) { + if (skeletonSizes[i] !== undefined) continue; + skeletonSizes[i] = SKELETON_EM_WIDTHS[i]; + } + } + + return skeletonSizes; + } + + generateSkeletonRow(skeletonSizes: Array, rowID: number): Array { + return skeletonSizes.map((width, j) => + ); + } + + generateSkeletonRows(skeletonSizes: Array): Array { + return new Array(SKELETON_ROWS).fill(null).map((_, i) => + {this.generateSkeletonRow(skeletonSizes, i)}); + } + render(): ReactNode { const { headers } = this.props; - const { orderBy, order } = this.state; + const { orderBy, order, loading } = this.state; - return + // Generate the skeleton rows + const skeletonSizes = this.generateSkeletonSizes(); + const skeletonRows = skeletonSizes ? this.generateSkeletonRows(skeletonSizes) : null; + + // Render the table + return
{/* Table headers, defined by the header map in the props */} {headers && @@ -54,32 +106,38 @@ {/* Table rows */} - - - - - - - - + {/* Render skeleton rows if the table is loading loading */} + {skeletonRows} - - - - - - - - + {/* Otherwise, render the data */} + {!loading && <> + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + }
Shop Walletkreichdyes12Shops2020/09/11 08:08
Main Walletkhugepoopy32016/02/14 00:00
Shop Walletkreichdyes12Shops2020/09/11 08:08
Old Walletkre3w0i79j02015/02/14 00:00
Main Walletkhugepoopy32016/02/14 00:00
Old Walletkre3w0i79j02015/02/14 00:00
; } diff --git a/src/shared-components/list-view/index.tsx b/src/shared-components/list-view/index.tsx index 2031bbd..9b580ce 100644 --- a/src/shared-components/list-view/index.tsx +++ b/src/shared-components/list-view/index.tsx @@ -16,6 +16,8 @@ export interface HeaderSpec { name: string; sortable?: boolean; + + skeletonEmWidth?: number; } interface Props { diff --git a/src/shared-components/skeleton/SkeletonText.tsx b/src/shared-components/skeleton/SkeletonText.tsx new file mode 100644 index 0000000..c42266c --- /dev/null +++ b/src/shared-components/skeleton/SkeletonText.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +import "./index.scss"; + +interface Props { + emWidth?: number; +} + +export const SkeletonText: React.FC = (props: Props) => ( +
+); diff --git a/src/shared-components/skeleton/index.scss b/src/shared-components/skeleton/index.scss new file mode 100644 index 0000000..420d4ea --- /dev/null +++ b/src/shared-components/skeleton/index.scss @@ -0,0 +1,32 @@ +@import "~scss/variables"; + +@keyframes skeleton-pulse { + 0% { + opacity: 1; + } + + 50% { + opacity: $skeleton-pulse-opacity; + } + + 100% { + opacity: 1; + } +} + +.skeleton { + background-color: $skeleton-color; + animation: skeleton-pulse $skeleton-pulse-duration ease-in-out $skeleton-pulse-delay infinite; + + &.skeleton-text { + // Make it take up the space of a normal line + display: inline-block; + line-height: 1; + + width: 100%; + max-width: $skeleton-max-width; + height: $skeleton-height; + + border-radius: $skeleton-height; + } +}