Skip to content

Commit 40ef0de

Browse files
committed
GAME-107 and GAME-99
1 parent c3ae18a commit 40ef0de

File tree

13 files changed

+187
-43
lines changed

13 files changed

+187
-43
lines changed

src-ts/lib/table/Table.module.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@
6161
margin-right: -29px;
6262
}
6363
}
64+
65+
&.centerHeader {
66+
justify-content: center;
67+
}
6468
}
6569

6670
.tooltip {
@@ -90,3 +94,8 @@
9094
.tootlipBody {
9195
min-width: 200px;
9296
}
97+
98+
.loadBtnWrap {
99+
display: flex;
100+
justify-content: center;
101+
}

src-ts/lib/table/Table.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import classNames from 'classnames'
22
import { Dispatch, MouseEvent, SetStateAction, useEffect, useState } from 'react'
33

4+
import { Button, ButtonSize, ButtonStyle } from '../button'
45
import { Sort } from '../pagination'
56
import '../styles/_includes.scss'
67
import { IconOutline } from '../svgs'
@@ -15,7 +16,13 @@ import styles from './Table.module.scss'
1516
interface TableProps<T> {
1617
readonly columns: ReadonlyArray<TableColumn<T>>
1718
readonly data: ReadonlyArray<T>
19+
readonly loadMoreBtnLabel?: string
20+
readonly loadMoreBtnSize?: ButtonSize
21+
readonly loadMoreBtnStyle?: ButtonStyle
22+
readonly moreToLoad?: boolean
23+
readonly onLoadMoreClick?: (data: T) => void
1824
readonly onRowClick?: (data: T) => void
25+
readonly onToggleSort?: (sort: Sort | undefined) => void
1926
}
2027

2128
interface DefaultSortDirectionMap {
@@ -75,6 +82,11 @@ const Table: <T extends { [propertyName: string]: any }>(props: TableProps<T>) =
7582
fieldName,
7683
}
7784
setSort(newSort)
85+
86+
// call the callback to notify parent for sort update
87+
if (props.onToggleSort) {
88+
props.onToggleSort(newSort)
89+
}
7890
}
7991

8092
const headerRow: Array<JSX.Element> = props.columns
@@ -83,12 +95,13 @@ const Table: <T extends { [propertyName: string]: any }>(props: TableProps<T>) =
8395
const isCurrentlySorted: boolean = isSortable && col.propertyName === sort?.fieldName
8496
const colorClass: string = isCurrentlySorted ? 'black-100' : 'black-60'
8597
const sortableClass: string | undefined = isSortable ? styles.sortable : undefined
98+
const centerClass: string | undefined = col.centerHeader ? styles.centerHeader : undefined
8699
return (
87100
<th
88101
className={styles.th}
89102
key={index}
90103
>
91-
<div className={classNames(styles['header-container'], styles[col.type], colorClass, sortableClass)}>
104+
<div className={classNames(styles['header-container'], styles[col.type], colorClass, sortableClass, centerClass)}>
92105
{col.label}
93106
{!!col.tooltip && (
94107
<div className={styles.tooltip}>
@@ -136,7 +149,7 @@ const Table: <T extends { [propertyName: string]: any }>(props: TableProps<T>) =
136149
// return the entire row
137150
return (
138151
<tr
139-
className={classNames(styles.tr, !!onRowClick ? styles.clickable : undefined)}
152+
className={classNames(styles.tr, props.onRowClick ? styles.clickable : undefined)}
140153
onClick={onRowClick}
141154
key={index}
142155
>
@@ -158,6 +171,11 @@ const Table: <T extends { [propertyName: string]: any }>(props: TableProps<T>) =
158171
{rowCells}
159172
</tbody>
160173
</table>
174+
{
175+
props.moreToLoad && <div className={styles['loadBtnWrap']}>
176+
<Button buttonStyle={props.loadMoreBtnStyle} label={props.loadMoreBtnLabel} size={props.loadMoreBtnSize} onClick={props.onLoadMoreClick} />
177+
</div>
178+
}
161179
</div>
162180
)
163181
}

src-ts/lib/table/table-column.model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { TableCellType } from './table-cell.type'
22

33
export interface TableColumn<T> {
4+
readonly centerHeader?: boolean
45
readonly defaultSortDirection?: 'asc' | 'desc'
56
readonly isDefaultSort?: boolean
67
readonly label?: string
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export interface Badge {
2+
active: boolean
3+
badge_description: string
4+
badge_image_url: string
5+
badge_name: string
6+
badge_status: string
7+
id: string
8+
organization_id: string
9+
}

src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { toolTitle } from '../../GamificationAdmin'
77
import styles from './BadgeDetailPage.module.scss'
88

99
const BadgeDetailPage: FC = () => {
10+
// TDOD: use whit GAME-78
1011
// const { id: badgeID } : { badgeID: string } = useParams()
1112
const breadcrumb: Array<BreadcrumbItemModel> = useMemo(() => [
1213
{ name: toolTitle, url: baseUrl },

src-ts/tools/gamification-admin/pages/badge-listing/BadgeListingPage.tsx

Lines changed: 35 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,51 @@
1-
import { FC, useState } from 'react'
1+
import { flatten, map } from 'lodash'
2+
import { Dispatch, FC, SetStateAction, useState } from 'react'
23
import { NavigateFunction, useNavigate } from 'react-router-dom'
34
// tslint:disable-next-line
45
import useSWRInfinite from 'swr/infinite'
56

6-
import { Button, ButtonProps, ContentLayout, IconOutline, LoadingSpinner } from '../../../../lib'
7+
import { ButtonProps, ContentLayout, LoadingSpinner, Sort, Table, TableColumn } from '../../../../lib'
78
import { GamificationConfig } from '../../config'
89
import { baseUrl } from '../../gamification-admin.routes'
910
import getDataSource from '../../lib/hooks/getDataSource'
11+
import { Badge } from '../../lib/models/badge.model'
1012

13+
import { badgeListingColumns } from './badge-listing-table/badge-listing-table.config'
1114
import styles from './BadgeListingPage.module.scss'
1215

1316
const BadgeListingPage: FC = () => {
1417
const [order, setOrder]: any = useState({ by: 'badge_name', type: 'asc' })
1518
const navigate: NavigateFunction = useNavigate()
1619
const dataSource: string = getDataSource()
20+
21+
// server-side pagination hook
1722
const getKey: any = (pageIndex: any, previousPageData: any) => {
1823
if (previousPageData && !previousPageData.rows.length) { return undefined } // reached the end
1924
return `${dataSource}/badges?organization_id=${GamificationConfig.ORG_ID}&limit=12&offset=${pageIndex * 12}&order_by=${order.by}&order_type=${order.type}`
2025
}
2126
const { data: badges, size, setSize }: any = useSWRInfinite(getKey, { revalidateFirstPage: false })
22-
const loadedCnt: any = badges?.reduce((ps: any, a: any) => ps + a.rows.length, 0)
23-
const onOrderClick: any = () => {
27+
28+
const tableData: Array<Badge> = flatten(map(badges, page => page.rows)) // flatten version of badges paginated data
29+
const loadedCnt: any = badges?.reduce((ps: any, a: any) => ps + a.rows.length, 0) // how much data is loaded so far
30+
31+
// listing table config
32+
const [columns]: [
33+
ReadonlyArray<TableColumn<Badge>>,
34+
Dispatch<SetStateAction<ReadonlyArray<TableColumn<Badge>>>>,
35+
]
36+
= useState<ReadonlyArray<TableColumn<Badge>>>([...badgeListingColumns])
37+
38+
// on sort toggle callback
39+
const onOrderClick: any = (sort: Sort) => {
2440
setOrder({
25-
by: order.by,
26-
type: order.type === 'asc' ? 'desc' : 'asc',
41+
by: sort.fieldName,
42+
type: sort.direction,
2743
})
2844
}
2945

46+
// on load more callback
47+
const onLoadMoreClick: any = () => setSize(size + 1)
48+
// header button config
3049
const buttonConfig: ButtonProps = {
3150
label: 'Create New Badge',
3251
onClick: () => navigate(`${baseUrl}/create-badge`),
@@ -40,41 +59,16 @@ const BadgeListingPage: FC = () => {
4059
buttonConfig={buttonConfig}
4160
>
4261
<div className={styles.container}>
43-
<div className={styles['badges-table-header']}>
44-
<div className={styles['col-sort']}>
45-
BADGE NAME
46-
{
47-
order.type === 'asc' ? (
48-
<Button icon={IconOutline.SortDescendingIcon} onClick={onOrderClick} buttonStyle='icon' />
49-
) : (
50-
<Button icon={IconOutline.SortAscendingIcon} onClick={onOrderClick} buttonStyle='icon' />
51-
)
52-
}
53-
</div>
54-
<div>ACTIONS</div>
55-
</div>
56-
<div className={styles['badges-table']}>
57-
{
58-
badges.map((page: any) => page.rows.map((badge: any) => <div className={styles['badge-row']} key={badge.id}>
59-
<div className={styles.badge}>
60-
<img src={badge.badge_image_url} alt={badge.badge_name} className={styles[badge.active ? 'badge-image' : 'badge-image-disabled']} />
61-
<p className={styles['badge-name']}>{badge.badge_name}</p>
62-
</div>
63-
<div className={styles.actions}>
64-
<Button buttonStyle='secondary' className={styles['action-btn']} label='View' size='sm' route={`${baseUrl}/badge-detail/${badge.id}`} />
65-
<Button buttonStyle='secondary' className={styles['action-btn']} label='Award' size='sm' onClick={() => { }} />
66-
</div>
67-
</div>
68-
))
69-
}
70-
</div>
71-
{
72-
badges[0].count !== loadedCnt && <div className={styles['loadbtn-wrap']}>
73-
<Button buttonStyle='tertiary' label='Load more' size='lg' onClick={() => {
74-
setSize(size + 1)
75-
}} />
76-
</div>
77-
}
62+
<Table
63+
columns={columns}
64+
data={tableData}
65+
onLoadMoreClick={onLoadMoreClick}
66+
loadMoreBtnStyle='tertiary'
67+
loadMoreBtnSize='lg'
68+
loadMoreBtnLabel='Load More'
69+
moreToLoad={badges[0].count !== loadedCnt}
70+
onToggleSort={onOrderClick}
71+
/>
7872
</div>
7973
</ContentLayout>
8074
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
@import "../../../../../../lib/styles/includes";
2+
@import "../../../../../../lib/styles/variables";
3+
4+
.badge-actions {
5+
display: flex;
6+
align-items: center;
7+
justify-content: center;
8+
padding-top: $space-lg;
9+
10+
@include ltemd {
11+
flex-direction: column;
12+
align-items: flex-end;
13+
}
14+
15+
a {
16+
margin-right: $space-sm;
17+
18+
@include ltemd {
19+
margin-right: 0;
20+
margin-bottom: $space-sm;
21+
}
22+
23+
&:last-child {
24+
margin-right: 0;
25+
}
26+
}
27+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Button, useCheckIsMobile } from '../../../../../../lib'
2+
import { baseUrl } from '../../../../gamification-admin.routes'
3+
import { Badge } from '../../../../lib/models/badge.model'
4+
5+
import styles from './BadgeActionRenderer.module.scss'
6+
7+
function BadgeActionRenderer(badge: Badge): JSX.Element {
8+
const isMobile: boolean = useCheckIsMobile()
9+
10+
return (
11+
<div className={styles['badge-actions']}>
12+
<Button buttonStyle='secondary' size={isMobile ? 'xs' : 'sm'} label='View' route={`${baseUrl}/badge-detail/${badge.id}`} />
13+
<Button buttonStyle='secondary' size={isMobile ? 'xs' : 'sm'} label='Edit' route={`${baseUrl}/badge-detail/${badge.id}#edit`} />
14+
<Button buttonStyle='secondary' size={isMobile ? 'xs' : 'sm'} label='Award' route={`${baseUrl}/badge-detail/${badge.id}#award`} />
15+
</div>
16+
)
17+
}
18+
19+
export default BadgeActionRenderer
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as BadgeActionRenderer } from './BadgeActionRenderer'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { TableColumn } from '../../../../../lib'
2+
import { Badge } from '../../../lib/models/badge.model'
3+
4+
import { BadgeActionRenderer } from './badge-action-renderer'
5+
import { BadgeListingNameRenderer } from './badge-name-renderer'
6+
7+
export const badgeListingColumns: ReadonlyArray<TableColumn<Badge>> = [
8+
{
9+
defaultSortDirection: 'asc',
10+
isDefaultSort: true,
11+
label: 'Badge Name',
12+
propertyName: 'badge_name',
13+
renderer: BadgeListingNameRenderer,
14+
type: 'element',
15+
},
16+
{
17+
centerHeader: true,
18+
label: 'Actions',
19+
renderer: BadgeActionRenderer,
20+
type: 'action',
21+
},
22+
]

0 commit comments

Comments
 (0)