11import ClosedBoards from "@/components/board/ClosedBoards" ;
22import CreateBoard from "@/components/shared/CreateBoard" ;
3+ import LoadingSpinner from "@/components/ui/LoadingSpinner" ;
4+ import { importBoard } from "@/services/boardService" ;
35import { notify } from "@/services/toastService" ;
46import { getBoards , getClosedBoards , getWorkspaceById , updateWorkspace } from "@/services/workspaceService" ;
57import type { Board } from "@/types/project" ;
68import type { WorkSpace } from "@/types/workspace" ;
7- import { Pencil , Users } from "lucide-react" ;
9+ import { Pencil , Upload , Users } from "lucide-react" ;
810import { useEffect , useState , type SetStateAction } from "react" ;
911import { useNavigate , useParams } from "react-router-dom" ;
1012
@@ -26,6 +28,7 @@ const Boards = () => {
2628 const [ workspaceNameError , setWorkspaceNameError ] = useState ( '' ) ;
2729 const [ workspaceDescriptionError , setWorkspaceDescriptionError ] = useState ( '' ) ;
2830 const [ showClosedBoards , setShowClosedBoards ] = useState ( false ) ;
31+ const [ isImportingBoard , setIsImportingBoard ] = useState ( false ) ;
2932
3033 const isWorkspaceChanged = (
3134 original : WorkSpace ,
@@ -89,7 +92,7 @@ const Boards = () => {
8992 . catch ( _ => navigate ( '/not-found' ) ) ;
9093 } ;
9194
92- const fetchBoards = async ( ) => {
95+ const fetchBoards = async ( ) => {
9396 if ( ! id ) return ;
9497 return await getBoards ( Number ( id ) )
9598 . then ( data => {
@@ -106,6 +109,54 @@ const Boards = () => {
106109 . catch ( err => notify . error ( err ?. message ) )
107110 } ;
108111
112+ const handleFileClick = ( ) => {
113+ // create hidden file input element
114+ const input = document . createElement ( "input" ) ;
115+ input . type = "file" ;
116+ input . accept = "application/json" ;
117+ input . style . display = "none" ;
118+
119+ // listen for file selection event
120+ input . onchange = async ( e ) => {
121+ const target = e . target as HTMLInputElement ;
122+ const file = target . files ?. [ 0 ] ;
123+ if ( ! file ) {
124+ console . log ( "User canceled file selection." ) ;
125+ return ;
126+ }
127+
128+ // validate file type
129+ const fileName = file . name . toLowerCase ( ) ;
130+ if ( ! fileName . endsWith ( ".json" ) ) {
131+ notify . error ( "Please select a valid JSON file." ) ;
132+ return ;
133+ }
134+ else {
135+ try {
136+ setBoards ( [ ...boards , {
137+ id : - 1 ,
138+ name : file . name
139+ } ] ) ;
140+ setIsImportingBoard ( true ) ;
141+ const response = await importBoard ( workspaceData . id , file ) ;
142+ notify . success ( response . message ) ;
143+ setBoards ( [ ...boards . filter ( b => b . id !== - 1 ) , response . data ] ) ;
144+ }
145+ catch ( error : any ) {
146+ notify . error ( error . response ?. data ?. message || 'Failed to import board!' )
147+ }
148+ finally {
149+ setIsImportingBoard ( false ) ;
150+ }
151+
152+ }
153+
154+ } ;
155+
156+ // open file dialog
157+ input . click ( ) ;
158+ } ;
159+
109160 useEffect ( ( ) => {
110161 if ( ! id ) return ;
111162 Promise . all ( [
@@ -192,32 +243,78 @@ const Boards = () => {
192243
193244 < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 gap-4" >
194245 { /* Existing boards */ }
195- { boards ?. map ( board => (
196- < div
197- key = { board . id }
198- onClick = { ( ) => handleBoardNavigate ( board . id ) }
199- className = { `
246+ { boards ?. map ( board => {
247+ if ( board . id === - 1 ) {
248+ return (
249+ // <button
250+ // className="
251+ // h-24 bg-[#2A2D31] hover:bg-[#3A3D41] rounded-lg cursor-pointer transition-colors
252+ // flex items-center justify-center border-2 border-dashed border-gray-600"
253+ // disabled={isImportingBoard}
254+ // >
255+ // {/* <LoadingSpinner /> */}
256+ // <div className='animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4'></div>
257+ // </button>
258+
259+ < div
260+ key = { board . id }
261+ className = { `
200262 flex items-end
201263 h-24 rounded-lg cursor-pointer overflow-hidden
202264 hover:opacity-90 transition-opacity ` }
203- style = { {
204- backgroundImage : `url(${ backgroundImage } )` ,
205- backgroundSize : 'cover' ,
206- backgroundPosition : 'center'
207- } }
208- >
209- < div className = "grow p-2 bg-black/50 flex items-end justify-between" >
210- < h3 className = "text-white font-medium text-sm" > { board . name } </ h3 >
265+ style = { {
266+ backgroundImage : `url(${ backgroundImage } )` ,
267+ backgroundSize : 'cover' ,
268+ backgroundPosition : 'center'
269+ } }
270+ >
271+ < div className = "grow p-2 bg-black/50 flex items-end justify-between" >
272+ < div className = 'animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4' > </ div >
273+ < h3 className = "text-white font-medium text-sm" > Importing { board . name } ...</ h3 >
274+ </ div >
275+ </ div >
276+
277+ )
278+ }
279+ return (
280+ < div
281+ key = { board . id }
282+ onClick = { ( ) => handleBoardNavigate ( board . id ) }
283+ className = { `
284+ flex items-end
285+ h-24 rounded-lg cursor-pointer overflow-hidden
286+ hover:opacity-90 transition-opacity ` }
287+ style = { {
288+ backgroundImage : `url(${ backgroundImage } )` ,
289+ backgroundSize : 'cover' ,
290+ backgroundPosition : 'center'
291+ } }
292+ >
293+ < div className = "grow p-2 bg-black/50 flex items-end justify-between" >
294+ < h3 className = "text-white font-medium text-sm" > { board . name } </ h3 >
295+ </ div >
211296 </ div >
212- </ div >
213- ) ) }
297+ )
298+ } ) }
214299
215300 { /* Create new board button */ }
216301 < CreateBoard
217302 id = { id ? Number ( id ) : null }
218303 workspaceName = { workspaceData . name }
219304 onBoardCreated = { fetchBoards }
220305 />
306+
307+ { /* Import new board button */ }
308+ < button
309+ onClick = { handleFileClick }
310+ className = "
311+ h-24 bg-[#2A2D31] hover:bg-[#3A3D41] rounded-lg cursor-pointer transition-colors
312+ flex items-center justify-center border-2 border-dashed border-gray-600"
313+ disabled = { isImportingBoard }
314+ >
315+ < span className = "text-gray-400 font-medium mr-2" > < Upload /> </ span >
316+ < span className = "text-gray-400 font-medium" > { isImportingBoard ? 'Importing...' : 'Import board' } </ span >
317+ </ button >
221318 </ div >
222319
223320 { /* View closed boards button */ }
@@ -252,9 +349,9 @@ const Boards = () => {
252349 </ div >
253350
254351 { /* Closed Boards Modal */ }
255- { showClosedBoards &&
256- < ClosedBoards
257- hideClosedBoards = { ( ) => setShowClosedBoards ( false ) }
352+ { showClosedBoards &&
353+ < ClosedBoards
354+ hideClosedBoards = { ( ) => setShowClosedBoards ( false ) }
258355 />
259356 }
260357 </ div >
0 commit comments