Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src-ts/lib/pagination/infinite-page-dao.model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export interface InfinitePageDao<T> {
count: number
limit: number
offset: number
// TODO: rename this 'items' so it can be used in a grid/card view
rows: ReadonlyArray<T>
}
28 changes: 15 additions & 13 deletions src-ts/tools/gamification-admin/game-lib/game-badge.model.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
// TODO: add factory to convert snake case property names to camel case
export interface MemberBadgeAward {
awarded_at: string
awarded_by: string
user_handle: string
user_id: string
}

export interface GameBadge {
active: boolean
badge_description: string
badge_image_url: string
badge_name: string
badge_status: string
id: string
member_badges?: Array<{
awarded_at: string,
awarded_by: string,
user_handle: string,
user_id: string,
}>
organization_id: string
active: boolean
badge_description: string
badge_image_url: string
badge_name: string
badge_status: string
id: string
member_badges?: Array<MemberBadgeAward>
organization_id: string
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BreadcrumbItemModel } from '../../../lib'
import { basePath } from '../gamification-admin.routes'
import { toolTitle } from '../GamificationAdmin'
import { BreadcrumbItemModel } from '../../../../lib'
import { basePath } from '../../gamification-admin.routes'
import { toolTitle } from '../../GamificationAdmin'

export function useGamificationBreadcrumb(items: Array<BreadcrumbItemModel>): Array<BreadcrumbItemModel> {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { EnvironmentConfig } from '../../../../config'
import { InfinitePageDao, InfinitePageHandler, Sort, useGetInfinitePage } from '../../../../lib'
import { GamificationConfig } from '../../game-config'
import { GameBadge, MemberBadgeAward } from '../game-badge.model'

export function useGetGameBadgeAssigneesPage(badge: GameBadge, sort: Sort): InfinitePageHandler<MemberBadgeAward> {

function getKey(index: number, previousPageData: InfinitePageDao<MemberBadgeAward>): string | undefined {

// reached the end
if (!!previousPageData && !previousPageData.rows.length) {
return undefined
}

const params: Record<string, string> = {
limit: `${GamificationConfig.PAGE_SIZE}`,
offset: `${index * GamificationConfig.PAGE_SIZE}`,
order_by: sort.fieldName,
order_type: sort.direction,
}

const badgeEndpointUrl: URL = new URL(`${EnvironmentConfig.API.V5}/gamification/badges/${badge.id}/assignees?${new URLSearchParams(params)}`)

return badgeEndpointUrl.toString()
}

return useGetInfinitePage(getKey)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import useSWR, { KeyedMutator, SWRResponse } from 'swr'

import { EnvironmentConfig } from '../../../config'

import { GameBadge } from './game-badge.model'
import { EnvironmentConfig } from '../../../../config'
import { GameBadge } from '../game-badge.model'

export interface BadgeDetailPageHandler<T> {
data?: Readonly<T>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { EnvironmentConfig } from '../../../config'
import { InfinitePageDao, InfinitePageHandler, Sort, useGetInfinitePage } from '../../../lib'
import { GamificationConfig } from '../game-config'

import { GameBadge } from './game-badge.model'
import { EnvironmentConfig } from '../../../../config'
import { InfinitePageDao, InfinitePageHandler, Sort, useGetInfinitePage } from '../../../../lib'
import { GamificationConfig } from '../../game-config'
import { GameBadge } from '../game-badge.model'

export function useGetGameBadgesPage(sort: Sort): InfinitePageHandler<GameBadge> {

Expand Down
6 changes: 3 additions & 3 deletions src-ts/tools/gamification-admin/game-lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './game-badge.model'
export * from './use-get-game-badges-page.hook'
export * from './use-gamification-breadcrumb.hook'
export * from './use-get-game-badge-details.hook'
export * from './hooks/use-get-game-badges-page.hook'
export * from './hooks/use-gamification-breadcrumb.hook'
export * from './hooks/use-get-game-badge-details.hook'
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
@import "../../../../../lib/styles/variables/spacing";

.tabWrap {
display: flex;
padding-bottom: $space-xxxxl;
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,54 @@
import { FC } from 'react'
import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'

import { GameBadge } from '../../../game-lib'
import { InfinitePageHandler, Sort, Table, TableColumn, tableGetDefaultSort } from '../../../../../lib'
import { GameBadge, MemberBadgeAward } from '../../../game-lib'
import { useGetGameBadgeAssigneesPage } from '../../../game-lib/hooks/use-get-game-badge-assignees-page.hook'

import { awardedMembersColumns } from './awarded-members-table/awarded-members-table.config'
import styles from './AwardedMembersTab.module.scss'

export interface AwardedMembersTabProps {
badge: GameBadge
forceRefresh?: boolean
}

const AwardedMembersTab: FC<AwardedMembersTabProps> = (props: AwardedMembersTabProps) => {
const [sort, setSort]: [Sort, Dispatch<SetStateAction<Sort>>] = useState<Sort>(tableGetDefaultSort(awardedMembersColumns))

const [columns]: [
ReadonlyArray<TableColumn<MemberBadgeAward>>,
Dispatch<SetStateAction<ReadonlyArray<TableColumn<MemberBadgeAward>>>>,
]
= useState<ReadonlyArray<TableColumn<MemberBadgeAward>>>([...awardedMembersColumns])

const pageHandler: InfinitePageHandler<MemberBadgeAward> = useGetGameBadgeAssigneesPage(props.badge, sort)

useEffect(() => {
if (props.forceRefresh && pageHandler) {
pageHandler.mutate()
}
}, [
props.forceRefresh,
pageHandler,
])

function onSortClick(newSort: Sort): void {
setSort({ ...newSort })
}

return (
<div className={styles.tabWrap}>

{
pageHandler.data?.length ? (
<Table
columns={columns}
data={pageHandler.data}
onLoadMoreClick={pageHandler.getAndSetNext}
moreToLoad={pageHandler.hasMore}
onToggleSort={onSortClick}
/>
) : undefined
}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { TableColumn } from '../../../../../../lib'
import { MemberBadgeAward } from '../../../../game-lib'

import { MemberActionRenderer } from './member-action-renderer'
import { MemberAwaredAtRenderer } from './member-awardedAt-renderer'
import { MemberHandleRenderer } from './member-handle-renderer'

export const awardedMembersColumns: ReadonlyArray<TableColumn<MemberBadgeAward>> = [
{
defaultSortDirection: 'asc',
isDefaultSort: true,
label: 'Handle',
propertyName: 'user_handle',
renderer: MemberHandleRenderer,
type: 'element',
},
{
defaultSortDirection: 'asc',
label: 'Awarded at',
propertyName: 'awarded_at',
renderer: MemberAwaredAtRenderer,
type: 'element',
},
{
renderer: MemberActionRenderer,
type: 'action',
},
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@import "../../../../../../../lib/styles/includes";
@import "../../../../../../../lib/styles/variables";

.badge-actions {
display: flex;
align-items: center;
justify-content: flex-end;
padding-top: $space-lg;
padding-right: $space-sm;

@include ltemd {
flex-direction: column;
align-items: flex-end;
}

a {
margin-right: $space-sm;

@include ltemd {
margin-right: 0;
margin-bottom: $space-sm;
}

&:last-child {
margin-right: 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Button, ButtonProps, useCheckIsMobile } from '../../../../../../../lib'
import { MemberBadgeAward } from '../../../../../game-lib'

import styles from './MemberActionRenderer.module.scss'

function MemberActionRenderer(memberAward: MemberBadgeAward): JSX.Element {

const isMobile: boolean = useCheckIsMobile()

const buttonProps: ButtonProps = {
buttonStyle: 'secondary',
size: isMobile ? 'xs' : 'sm',
}

const actionButtons: Array<{
label: string
}> = [
{
label: 'Unassign',
},
]

function onUnassign(): void {
// TODO: unassign feature
}

return (
<div className={styles['badge-actions']}>
{actionButtons.map((button, index) => {
return (
<Button
{...buttonProps}
key={index}
label={button.label}
onClick={onUnassign}
/>
)
})}
</div>
)
}

export default MemberActionRenderer
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as MemberActionRenderer } from './MemberActionRenderer'
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@import "../../../../../../../lib/styles/variables";
@import "../../../../../../../lib/styles/includes";

.memberAwardedAt {
font-size: 16px;
font-weight: $font-weight-normal;
text-align: left;

@include ltemd {
font-size: 14px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MemberBadgeAward } from '../../../../../game-lib'

import styles from './MemberAwaredAtRenderer.module.scss'

function MemberAwaredAtRenderer(memberAward: MemberBadgeAward): JSX.Element {
const dateFormat: Record<string, string> = {
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
month: 'short',
year: 'numeric',
}

return (
<div className={styles.memberAwardedAt}>{new Date(memberAward.awarded_at).toLocaleString(undefined, dateFormat)}</div>
)
}

export default MemberAwaredAtRenderer
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as MemberAwaredAtRenderer } from './MemberAwaredAtRenderer'
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@import "../../../../../../../lib/styles/includes";
@import "../../../../../../../lib/styles/variables";

.memberAward {
display: flex;
align-items: center;

.memberHandle {
font-size: 16px;
font-weight: 700;
line-height: 24px;
color: $black-100;
}

.profileLink {
cursor: pointer;
color: $turq-160;
width: 16px;
height: 16px;
margin-left: $space-xs;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { EnvironmentConfig } from '../../../../../../../config'
import { IconOutline } from '../../../../../../../lib'
import { MemberBadgeAward } from '../../../../../game-lib'

import styles from './MemberHandleRenderer.module.scss'

function MemberHandleRenderer(memberAward: MemberBadgeAward): JSX.Element {
return (
<div className={styles.memberAward}>
<p className={styles.memberHandle}>{memberAward.user_handle}</p>
<IconOutline.ExternalLinkIcon
className={styles.profileLink}
onClick={() => window.open(`${EnvironmentConfig.TOPCODER_URLS.USER_PROFILE}/${memberAward.user_handle}`, '_blank')}
/>
</div>
)
}

export default MemberHandleRenderer
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as MemberHandleRenderer } from './MemberHandleRenderer'
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ const BadgeDetailPage: FC = () => {

const [showActivatedModal, setShowActivatedModal]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false)

const [forceAwardedMembersTabRefresh, setForceAwardedMembersTabRefresh]: [boolean | undefined, Dispatch<SetStateAction<boolean | undefined>>]
= useState<boolean | undefined>()

useEffect(() => {
if (newImageFile && newImageFile.length) {
const fileReader: FileReader = new FileReader()
Expand Down Expand Up @@ -261,13 +264,22 @@ const BadgeDetailPage: FC = () => {
}
}

function onManualAssign(): void {
// refresh awardedMembers data
setForceAwardedMembersTabRefresh(true)
setActiveTab(BadgeDetailsTabViews.awardedMembers)
}

// default tab
let activeTabElement: JSX.Element
= <AwardedMembersTab badge={badgeDetailsHandler.data as GameBadge} />
= <AwardedMembersTab
badge={badgeDetailsHandler.data as GameBadge}
forceRefresh={forceAwardedMembersTabRefresh}
/>
if (activeTab === BadgeDetailsTabViews.manualAward) {
activeTabElement = <ManualAwardTab
badge={badgeDetailsHandler.data as GameBadge}
onManualAssign={() => setActiveTab(BadgeDetailsTabViews.awardedMembers)}
onManualAssign={onManualAssign}
/>
}
if (activeTab === BadgeDetailsTabViews.batchAward) {
Expand Down