1- import { Dispatch , FC , SetStateAction , useEffect , useState } from 'react'
2- import ReactMarkdown from 'react-markdown'
1+ import { noop , trim } from 'lodash'
2+ import MarkdownIt from 'markdown-it'
3+ import { createRef , Dispatch , FC , KeyboardEvent , RefObject , SetStateAction , useEffect , useState } from 'react'
4+ import ContentEditable from 'react-contenteditable'
35import { Params , useLocation , useParams } from 'react-router-dom'
6+ import { toast } from 'react-toastify'
47
5- import { Breadcrumb , BreadcrumbItemModel , ButtonProps , ContentLayout , LoadingSpinner , PageDivider , TabsNavbar , TabsNavItem } from '../../../../lib'
8+ import { Breadcrumb , BreadcrumbItemModel , Button , ButtonProps , ContentLayout , IconOutline , LoadingSpinner , PageDivider , TabsNavbar , TabsNavItem } from '../../../../lib'
9+ import { GamificationConfig } from '../../game-config'
610import { BadgeDetailPageHandler , GameBadge , useGamificationBreadcrumb , useGetGameBadgeDetails } from '../../game-lib'
711
812import { badgeDetailsTabs , BadgeDetailsTabViews } from './badge-details-tabs.config'
13+ import { submitRequestAsync as updateBadgeAsync } from './badge-details.functions'
914import styles from './BadgeDetailPage.module.scss'
1015
16+ const md : MarkdownIt = new MarkdownIt ( {
17+ html : true ,
18+ linkify : true ,
19+ typographer : true ,
20+ } )
21+
22+ /* tslint:disable:cyclomatic-complexity */
1123const BadgeDetailPage : FC = ( ) => {
1224 const [ headerButtonConfig , setHeaderButtonConfig ] : [
1325 ButtonProps | undefined ,
@@ -36,6 +48,55 @@ const BadgeDetailPage: FC = () => {
3648
3749 const badgeDetailsHandler : BadgeDetailPageHandler < GameBadge > = useGetGameBadgeDetails ( badgeID as string )
3850
51+ const badgeNameRef : RefObject < HTMLDivElement > = createRef < HTMLDivElement > ( )
52+
53+ const badgeDescRef : RefObject < HTMLDivElement > = createRef < HTMLDivElement > ( )
54+
55+ const fileInputRef : RefObject < HTMLInputElement > = createRef < HTMLInputElement > ( )
56+
57+ // tslint:disable-next-line:no-null-keyword
58+ const [ newImageFile , setNewImageFile ] : [ FileList | null , Dispatch < SetStateAction < FileList | null > > ] = useState < FileList | null > ( null )
59+
60+ const [ fileDataURL , setFileDataURL ] : [ string | undefined , Dispatch < SetStateAction < string | undefined > > ] = useState < string | undefined > ( )
61+
62+ const [ isBadgeDescEditingMode , setIsBadgeDescEditingMode ] : [ boolean , Dispatch < SetStateAction < boolean > > ] = useState < boolean > ( false )
63+
64+ useEffect ( ( ) => {
65+ if ( newImageFile && newImageFile . length ) {
66+ const fileReader : FileReader = new FileReader ( )
67+ fileReader . onload = e => {
68+ const { result } : any = e . target
69+ if ( result ) {
70+ setFileDataURL ( result )
71+ }
72+ }
73+ fileReader . readAsDataURL ( newImageFile [ 0 ] )
74+ } else if ( fileDataURL ) {
75+ setFileDataURL ( undefined )
76+ }
77+ } , [
78+ newImageFile ,
79+ fileDataURL ,
80+ ] )
81+
82+ useEffect ( ( ) => {
83+ if ( newImageFile && newImageFile . length ) {
84+ updateBadgeAsync ( {
85+ files : newImageFile as FileList ,
86+ id : badgeDetailsHandler . data ?. id as string ,
87+ } )
88+ . then ( ( updatedBadge : GameBadge ) => {
89+ toast . success ( 'Badge image file saved.' )
90+ badgeDetailsHandler . mutate ( {
91+ ...badgeDetailsHandler . data ,
92+ badge_image_url : updatedBadge . badge_image_url ,
93+ } )
94+ } )
95+ }
96+ } , [
97+ newImageFile ,
98+ ] )
99+
39100 useEffect ( ( ) => {
40101 if ( badgeDetailsHandler . data ) {
41102 switch ( badgeDetailsHandler . data ?. active ) {
@@ -57,6 +118,15 @@ const BadgeDetailPage: FC = () => {
57118 badgeDetailsHandler . data ,
58119 ] )
59120
121+ // define the tabs so they can be displayed on various results
122+ const tabsElement : JSX . Element = (
123+ < TabsNavbar
124+ tabs = { tabs }
125+ defaultActive = { activeTab }
126+ onChange = { onChangeTab }
127+ />
128+ )
129+
60130 function onChangeTab ( active : string ) : void {
61131 // TODO: implement in GAME-129
62132 }
@@ -69,14 +139,55 @@ const BadgeDetailPage: FC = () => {
69139 // TODO: implement in GAME-127
70140 }
71141
72- // define the tabs so they can be displayed on various results
73- const tabsElement : JSX . Element = (
74- < TabsNavbar
75- tabs = { tabs }
76- defaultActive = { activeTab }
77- onChange = { onChangeTab }
78- />
79- )
142+ function onNameEditKeyDown ( e : KeyboardEvent ) : void {
143+ if ( e . key === 'Enter' ) {
144+ e . preventDefault ( )
145+ badgeNameRef . current ?. blur ( )
146+ }
147+ }
148+
149+ function onBadgeNameEditFocus ( ) : void {
150+ if ( isBadgeDescEditingMode ) {
151+ setIsBadgeDescEditingMode ( false )
152+ }
153+ }
154+
155+ function onSaveBadgeName ( ) : any {
156+ const newBadgeName : string | undefined = trim ( badgeNameRef . current ?. innerHTML )
157+ if ( newBadgeName !== badgeDetailsHandler . data ?. badge_name ) {
158+ // save only if different
159+ updateBadgeAsync ( {
160+ badgeName : newBadgeName ,
161+ id : badgeDetailsHandler . data ?. id as string ,
162+ } )
163+ . then ( ( ) => {
164+ toast . success ( 'Badge name update saved.' )
165+ badgeDetailsHandler . mutate ( {
166+ ...badgeDetailsHandler . data ,
167+ badge_name : newBadgeName ,
168+ } )
169+ } )
170+ }
171+ }
172+
173+ function onSaveBadgeDesc ( ) : any {
174+ setIsBadgeDescEditingMode ( false )
175+ const newBadgeDesc : string | undefined = trim ( badgeDescRef . current ?. innerHTML )
176+ if ( newBadgeDesc !== badgeDetailsHandler . data ?. badge_description ) {
177+ // save only if different
178+ updateBadgeAsync ( {
179+ badgeDesc : newBadgeDesc ,
180+ id : badgeDetailsHandler . data ?. id as string ,
181+ } )
182+ . then ( ( ) => {
183+ toast . success ( 'Badge description update saved.' )
184+ badgeDetailsHandler . mutate ( {
185+ ...badgeDetailsHandler . data ,
186+ badge_description : newBadgeDesc ,
187+ } )
188+ } )
189+ }
190+ }
80191
81192 if ( ! badgeDetailsHandler . data && ! badgeDetailsHandler . error ) {
82193 return < LoadingSpinner />
@@ -98,12 +209,60 @@ const BadgeDetailPage: FC = () => {
98209 < >
99210 < div className = { styles . badge } >
100211 < div className = { styles . badgeImage } >
101- < img src = { badgeDetailsHandler . data ?. badge_image_url } alt = 'badge media preview' />
212+ < Button
213+ buttonStyle = 'icon'
214+ icon = { IconOutline . PencilIcon }
215+ className = { styles . filePickerPencil }
216+ onClick = { ( ) => fileInputRef . current ?. click ( ) } />
217+ < img src = { fileDataURL || badgeDetailsHandler . data ?. badge_image_url } alt = 'badge media preview' />
218+ < input
219+ type = { 'file' }
220+ ref = { fileInputRef }
221+ className = { styles . filePickerInput }
222+ accept = { GamificationConfig . ACCEPTED_BADGE_MIME_TYPES }
223+ size = { GamificationConfig . MAX_BADGE_IMAGE_FILE_SIZE }
224+ onChange = { e => setNewImageFile ( e . target . files ) }
225+ />
102226 </ div >
103227 < div className = { styles . badgeDetails } >
104- < h2 > { badgeDetailsHandler . data ?. badge_name } </ h2 >
228+ < ContentEditable
229+ innerRef = { badgeNameRef }
230+ html = { badgeDetailsHandler . data ?. badge_name as string }
231+ onChange = { noop }
232+ onKeyDown = { onNameEditKeyDown }
233+ onBlur = { onSaveBadgeName }
234+ onFocus = { onBadgeNameEditFocus }
235+ className = { styles . badgeName }
236+ />
105237 < div className = { styles . badgeDesc } >
106- < ReactMarkdown children = { badgeDetailsHandler . data ?. badge_description as string } />
238+ < div className = { styles . badgeEditWrap } >
239+ < ContentEditable
240+ innerRef = { badgeDescRef }
241+ html = {
242+ isBadgeDescEditingMode
243+ ? badgeDetailsHandler . data ?. badge_description as string
244+ : md . render ( badgeDetailsHandler . data ?. badge_description as string )
245+ }
246+ onChange = { noop }
247+ onFocus = { ( ) => setIsBadgeDescEditingMode ( true ) }
248+ className = { isBadgeDescEditingMode ? styles . badgeEditableMode : styles . badgeEditable }
249+ />
250+ {
251+ isBadgeDescEditingMode && < div className = { styles . badgeEditActions } >
252+ < Button
253+ label = 'Cancel'
254+ buttonStyle = 'secondary'
255+ size = 'xs'
256+ onClick = { ( ) => setIsBadgeDescEditingMode ( false ) }
257+ />
258+ < Button
259+ label = 'Save'
260+ size = 'xs'
261+ onClick = { onSaveBadgeDesc }
262+ />
263+ </ div >
264+ }
265+ </ div >
107266 </ div >
108267 </ div >
109268 </ div >
0 commit comments