feat(SW-441): Added table block component

This commit is contained in:
Erik Tiekstra
2024-10-04 11:51:39 +02:00
parent 4979c22c4a
commit 5e4ef02ebf
23 changed files with 431 additions and 29 deletions

View File

@@ -0,0 +1,97 @@
"use client"
import {
flexRender,
getCoreRowModel,
getPaginationRowModel,
useReactTable,
} from "@tanstack/react-table"
import { useState } from "react"
import ScrollWrapper from "@/components/TempDesignSystem/ScrollWrapper"
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
import Table from "@/components/TempDesignSystem/Table"
import styles from "./table.module.css"
import type { TableProps } from "@/types/components/blocks/table"
export default function TableBlock({ data }: TableProps) {
const { columns, rows, totalWidth } = data
const initialPageSize = 5
const [pageSize, setPageSize] = useState(initialPageSize)
const showMoreVisible = rows.length > initialPageSize
const showLessVisible = pageSize >= rows.length
const columnDefs = columns.map((col) => ({
accessorKey: col.id,
header: col.header,
size: col.width,
}))
const table = useReactTable({
columns: columnDefs,
data: rows,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
state: {
pagination: {
pageIndex: 0,
pageSize,
},
},
})
function handleShowMore() {
setPageSize(showLessVisible ? initialPageSize : rows.length)
}
return (
<div className={styles.tableWrapper}>
<ScrollWrapper>
<Table
width={`${totalWidth}%`}
variant="content"
intent="striped"
layout="fixed"
borderRadius="none"
>
<Table.THead>
{table.getHeaderGroups().map((headerGroup) => (
<Table.TR key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<Table.TH
key={header.id}
width={`${header.column.columnDef.size}%`}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</Table.TH>
))}
</Table.TR>
))}
</Table.THead>
<Table.TBody>
{table.getRowModel().rows.map((row) => (
<Table.TR key={row.id}>
{row.getVisibleCells().map((cell) => (
<Table.TD key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Table.TD>
))}
</Table.TR>
))}
</Table.TBody>
</Table>
</ScrollWrapper>
{showMoreVisible ? (
<ShowMoreButton
loadMoreData={handleShowMore}
showLess={showLessVisible}
intent="table"
/>
) : null}
</div>
)
}

View File

@@ -0,0 +1,6 @@
.tableWrapper {
display: grid;
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Small);
overflow: hidden;
}

View File

@@ -5,7 +5,7 @@ import TextCols from "@/components/Blocks/TextCols"
import UspGrid from "@/components/Blocks/UspGrid"
import JsonToHtml from "@/components/JsonToHtml"
import Table from "../Table"
import Table from "./Table"
import type { BlocksProps } from "@/types/components/blocks"
import { BlocksEnums } from "@/types/enums/blocks"
@@ -50,7 +50,7 @@ export default function Blocks({ blocks }: BlocksProps) {
/>
)
case BlocksEnums.block.Table:
return <Table columns={block.table.columns} rows={block.table.rows} />
return <Table data={block.table} />
case BlocksEnums.block.TextCols:
return <TextCols text_cols={block.text_cols} />
case BlocksEnums.block.TextContent:

View File

@@ -40,6 +40,8 @@
}
.mainContent {
display: grid;
gap: var(--Spacing-x4);
width: 100%;
}

View File

@@ -0,0 +1,35 @@
"use client"
import { useMemo } from "react"
import useScrollShadows from "@/hooks/useScrollShadows"
import { ScrollWrapperProps } from "./scrollWrapper"
import styles from "./scrollWrapper.module.css"
export default function ScrollWrapper({
className,
children,
}: ScrollWrapperProps) {
const { containerRef, showLeftShadow, showRightShadow } = useScrollShadows()
const classNames = useMemo(() => {
const cls = [styles.scrollWrapper, className]
if (showLeftShadow) {
cls.push(styles.leftShadow)
}
if (showRightShadow) {
cls.push(styles.rightShadow)
}
return cls.join(" ")
}, [showLeftShadow, showRightShadow, className])
return (
<div className={classNames}>
<div className={styles.content} ref={containerRef}>
{children}
</div>
</div>
)
}

View File

@@ -0,0 +1,33 @@
.scrollWrapper {
position: relative;
overflow: hidden;
}
.scrollWrapper::before,
.scrollWrapper::after {
content: "";
position: absolute;
top: 0;
height: 100%;
pointer-events: none;
z-index: 1;
transition: opacity 0.2s ease;
opacity: 0;
width: 50px;
}
.scrollWrapper.leftShadow::before {
left: 0;
background: linear-gradient(to right, rgba(0, 0, 0, 0.3), transparent);
opacity: 1;
}
.scrollWrapper.rightShadow::after {
right: 0;
background: linear-gradient(to left, rgba(0, 0, 0, 0.3), transparent);
opacity: 1;
}
.content {
overflow-x: auto;
}

View File

@@ -0,0 +1,2 @@
export interface ScrollWrapperProps
extends React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>> {}

View File

@@ -1,4 +0,0 @@
.container {
display: flex;
justify-content: center;
}

View File

@@ -5,18 +5,29 @@ import { useIntl } from "react-intl"
import { ChevronDownIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import styles from "./button.module.css"
import { showMoreButtonVariants } from "./variants"
import type { ShowMoreButtonParams } from "@/types/components/myPages/stays/button"
import styles from "./showMoreButton.module.css"
import type { ShowMoreButtonProps } from "./showMoreButton"
export default function ShowMoreButton({
className,
intent,
disabled,
showLess,
loadMoreData,
}: ShowMoreButtonParams) {
const { formatMessage } = useIntl()
}: ShowMoreButtonProps) {
const intl = useIntl()
const classNames = showMoreButtonVariants({
className,
intent,
})
return (
<div className={styles.container}>
<div className={`${classNames} ${showLess ? styles.showLess : ""}`}>
<Button
className={styles.button}
disabled={disabled}
onClick={loadMoreData}
variant="icon"
@@ -24,8 +35,8 @@ export default function ShowMoreButton({
theme="base"
intent="text"
>
<ChevronDownIcon />
{formatMessage({ id: "Show more" })}
<ChevronDownIcon className={styles.icon} />
{intl.formatMessage({ id: showLess ? "Show less" : "Show more" })}
</Button>
</div>
)

View File

@@ -0,0 +1,23 @@
.container {
display: flex;
justify-content: center;
}
.table {
display: grid;
justify-content: stretch;
border-top: 1px solid var(--Base-Border-Subtle);
background-color: var(--Base-Surface-Primary-light-Normal);
}
.table .button {
border-radius: 0;
}
.icon {
transition: transform 0.3s;
}
.showLess .icon {
transform: rotate(180deg);
}

View File

@@ -0,0 +1,11 @@
import { showMoreButtonVariants } from "./variants"
import type { VariantProps } from "class-variance-authority"
export interface ShowMoreButtonProps
extends React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>,
VariantProps<typeof showMoreButtonVariants> {
disabled?: boolean
showLess?: boolean
loadMoreData: () => void
}

View File

@@ -0,0 +1,11 @@
import { cva } from "class-variance-authority"
import styles from "./showMoreButton.module.css"
export const showMoreButtonVariants = cva(styles.container, {
variants: {
intent: {
table: styles.table,
},
},
})

View File

@@ -1,7 +1,13 @@
import styles from "./table.module.css"
function TH({ children }: React.PropsWithChildren) {
return <th className={styles.th}>{children}</th>
import type { THeadProps } from "./table"
function TH({ children, width = "auto", ...props }: THeadProps) {
return (
<th className={styles.th} style={{ width }} {...props}>
{children}
</th>
)
}
export default TH

View File

@@ -1,13 +1,34 @@
import { TableProps } from "./table"
import TBody from "./TBody"
import TD from "./TD"
import TH from "./TH"
import THead from "./THead"
import TR from "./TR"
import { tableVariants } from "./variants"
import styles from "./table.module.css"
function Table({
className,
intent,
borderRadius,
variant,
layout,
width = "100%",
children,
...props
}: TableProps) {
const classNames = tableVariants({
className,
borderRadius,
intent,
layout,
variant,
})
function Table({ children }: React.PropsWithChildren) {
return <table className={styles.table}>{children}</table>
return (
<table className={classNames} style={{ width }} {...props}>
{children}
</table>
)
}
Table.THead = THead

View File

@@ -1,20 +1,20 @@
.table {
border-radius: var(--Corner-radius-Medium);
border-collapse: collapse;
overflow: hidden;
width: 100%;
min-width: 100%;
}
.thead {
background-color: var(--Base-Background-Secondary-Normal, #f7e1d5);
color: var(--Base-Text-High-contrast);
background-color: var(--Base-Surface-Primary-dark-Normal);
}
.tbody {
background-color: var(--Base-Surface-Primary-light-Normal, #fff);
background-color: var(--Base-Surface-Primary-light-Normal);
}
.tr:not(:last-of-type) {
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider, #f0c1b6);
border-bottom: 1px solid var(--Base-Border-Subtle);
}
.th {
@@ -28,6 +28,35 @@
padding: var(--Spacing-x2);
}
.fixed {
table-layout: fixed;
}
.smallRadius {
border-radius: var(--Corner-radius-Small);
}
.mediumRadius {
border-radius: var(--Corner-radius-Medium);
}
.largeRadius {
border-radius: var(--Corner-radius-Large);
}
.content .thead {
background-color: var(--Base-Surface-Subtle-Hover);
}
.content .tbody {
background-color: var(--Base-Background-Primary-Normal);
}
.content.striped .tbody .tr:nth-child(odd) {
background-color: var(--Base-Surface-Subtle-Normal);
}
.content.striped .tbody .tr:nth-child(even) {
background-color: var(--Base-Background-Primary-Normal);
}
@media screen and (min-width: 768px) {
.th {
padding: var(--Spacing-x2) var(--Spacing-x3);

View File

@@ -0,0 +1,14 @@
import { tableVariants } from "./variants"
import type { VariantProps } from "class-variance-authority"
export interface TableProps
extends React.PropsWithChildren<React.HTMLAttributes<HTMLTableElement>>,
VariantProps<typeof tableVariants> {
width?: string
}
export interface THeadProps
extends React.PropsWithChildren<React.HTMLAttributes<HTMLTableCellElement>> {
width?: string
}

View File

@@ -0,0 +1,27 @@
import { cva } from "class-variance-authority"
import styles from "./table.module.css"
export const tableVariants = cva(styles.table, {
variants: {
intent: {
light: styles.light,
striped: styles.striped,
},
variant: {
content: styles.content,
},
borderRadius: {
none: "",
small: styles.smallRadius,
medium: styles.mediumRadius,
large: styles.largeRadius,
},
layout: {
fixed: styles.fixed,
},
},
defaultVariants: {
borderRadius: "medium",
},
})

36
hooks/useScrollShadows.ts Normal file
View File

@@ -0,0 +1,36 @@
import { useEffect, useRef, useState } from "react"
const useScrollShadows = () => {
const containerRef = useRef<HTMLDivElement | null>(null)
const [showLeftShadow, setShowLeftShadow] = useState<boolean>(false)
const [showRightShadow, setShowRightShadow] = useState<boolean>(false)
useEffect(() => {
const handleScroll = () => {
const container = containerRef.current
if (!container) return
setShowLeftShadow(container.scrollLeft > 0)
setShowRightShadow(
container.scrollLeft < container.scrollWidth - container.clientWidth
)
}
const container = containerRef.current
if (container) {
container.addEventListener("scroll", handleScroll)
}
handleScroll()
return () => {
if (container) {
container.removeEventListener("scroll", handleScroll)
}
}
}, [])
return { containerRef, showLeftShadow, showRightShadow }
}
export default useScrollShadows

32
package-lock.json generated
View File

@@ -23,6 +23,7 @@
"@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.9",
"@t3-oss/env-nextjs": "^0.9.2",
"@tanstack/react-query": "^5.28.6",
"@tanstack/react-table": "^8.20.5",
"@trpc/client": "^11.0.0-rc.467",
"@trpc/react-query": "^11.0.0-rc.467",
"@trpc/server": "^11.0.0-rc.467",
@@ -5971,6 +5972,37 @@
"react": "^18.0.0"
}
},
"node_modules/@tanstack/react-table": {
"version": "8.20.5",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz",
"integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==",
"dependencies": {
"@tanstack/table-core": "8.20.5"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/@tanstack/table-core": {
"version": "8.20.5",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz",
"integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@testing-library/dom": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.1.0.tgz",

View File

@@ -39,6 +39,7 @@
"@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.9",
"@t3-oss/env-nextjs": "^0.9.2",
"@tanstack/react-query": "^5.28.6",
"@tanstack/react-table": "^8.20.5",
"@trpc/client": "^11.0.0-rc.467",
"@trpc/react-query": "^11.0.0-rc.467",
"@trpc/server": "^11.0.0-rc.467",

View File

@@ -28,17 +28,20 @@ export const tableSchema = z.object({
}),
})
.transform((data) => {
const totalWidth = data.column_widths.reduce(
(acc, width) => acc + width,
0
)
const columns = data.table.tableState.columns.map((col, idx) => ({
id: col.id,
Header: col.label || "",
accessor: col.accessor,
columnWidth: data.column_widths[idx] || 0,
header: col.label || "",
width: data.column_widths[idx] || 0,
}))
const rows = data.table.tableState.data.map((rowData) => {
const transformedRow: Record<string, string> = {}
columns.forEach((col) => {
transformedRow[col.accessor] = rowData[col.accessor] || ""
columns.forEach((header) => {
transformedRow[header.id] = rowData[header.id] || ""
})
return transformedRow
})
@@ -46,6 +49,7 @@ export const tableSchema = z.object({
return {
columns,
rows,
totalWidth,
}
}),
})

View File

@@ -0,0 +1,5 @@
import type { TableData } from "@/types/trpc/routers/contentstack/blocks"
export interface TableProps {
data: TableData
}

View File

@@ -14,6 +14,6 @@ export interface DynamicContent extends z.output<typeof dynamicContentSchema> {}
export interface Shortcuts extends z.output<typeof shortcutsSchema> {}
export type Shortcut = Shortcuts["shortcuts"]
export interface TableBlock extends z.output<typeof tableSchema> {}
export type Table = TableBlock["table"]
export type TableData = TableBlock["table"]
export interface TextCols extends z.output<typeof textColsSchema> {}
export interface UspGrid extends z.output<typeof uspGridSchema> {}