Skip to content

Commit 4c8b2cb

Browse files
authored
import project data UI (#39)
1 parent 5e34d64 commit 4c8b2cb

File tree

4 files changed

+161
-55
lines changed

4 files changed

+161
-55
lines changed

frontend/src/components/shared/CreateBoard.tsx

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createBoard } from '@/services/workspaceService';
2-
import { TriangleAlert } from 'lucide-react';
2+
import { Plus, TriangleAlert } from 'lucide-react';
33
import { useState } from 'react';
44
import { notify } from '@/services/toastService';
55

@@ -16,9 +16,9 @@ const backgroundOptions = [
1616
];
1717

1818
type CreateBoardProp = {
19-
id: number | null;
20-
workspaceName: string;
21-
onBoardCreated?: () => void;
19+
id: number | null;
20+
workspaceName: string;
21+
onBoardCreated?: () => void;
2222
};
2323

2424
const CreateBoard: React.FC<CreateBoardProp> = ({ id, workspaceName, onBoardCreated }) => {
@@ -30,32 +30,32 @@ const CreateBoard: React.FC<CreateBoardProp> = ({ id, workspaceName, onBoardCrea
3030
const [isLoading, setIsLoading] = useState(false);
3131

3232
const handleCreateBoard = async () => {
33-
if(!id) return;
33+
if (!id) return;
3434
if (boardTitle.trim()) {
3535
setIsLoading(true);
3636
setErrorMessage(''); // Clear any previous errors
37-
37+
3838
await createBoard(boardTitle, id)
39-
.then(res => {
40-
notify.success(res.message);
41-
setShowCreateModal(false);
42-
onBoardCreated?.()
43-
setBoardTitle('');
44-
setSelectedBackground(0);
45-
setErrorMessage(''); // Clear errors on success
46-
})
47-
.catch(err => {
48-
// Handle different error response structures
49-
const apiErrorMessage = err.response?.data?.errors?.[0]?.message
50-
|| err.response?.data?.message
51-
|| err.message
52-
|| 'An error occurred while creating the board';
53-
setErrorMessage(apiErrorMessage);
54-
notify.error(apiErrorMessage);
55-
})
56-
.finally(() => {
57-
setIsLoading(false);
58-
});
39+
.then(res => {
40+
notify.success(res.message);
41+
setShowCreateModal(false);
42+
onBoardCreated?.()
43+
setBoardTitle('');
44+
setSelectedBackground(0);
45+
setErrorMessage(''); // Clear errors on success
46+
})
47+
.catch(err => {
48+
// Handle different error response structures
49+
const apiErrorMessage = err.response?.data?.errors?.[0]?.message
50+
|| err.response?.data?.message
51+
|| err.message
52+
|| 'An error occurred while creating the board';
53+
setErrorMessage(apiErrorMessage);
54+
notify.error(apiErrorMessage);
55+
})
56+
.finally(() => {
57+
setIsLoading(false);
58+
});
5959
}
6060
};
6161

@@ -94,7 +94,8 @@ const CreateBoard: React.FC<CreateBoardProp> = ({ id, workspaceName, onBoardCrea
9494
className="
9595
h-24 bg-[#2A2D31] hover:bg-[#3A3D41] rounded-lg cursor-pointer transition-colors
9696
flex items-center justify-center border-2 border-dashed border-gray-600"
97-
>
97+
>
98+
<span className="text-gray-400 font-medium mr-0.5"><Plus /></span>
9899
<span className="text-gray-400 font-medium">Create new board</span>
99100
</div>
100101
{/* Create Board Modal */}
@@ -153,9 +154,8 @@ const CreateBoard: React.FC<CreateBoardProp> = ({ id, workspaceName, onBoardCrea
153154
type="text"
154155
value={boardTitle}
155156
onChange={handleTitleChange}
156-
className={`w-full px-3 py-2 bg-[#1E2125] border rounded text-white text-sm focus:outline-none ${
157-
errorToDisplay ? 'border-red-400 focus:border-red-400' : 'border-gray-600 focus:border-blue-400'
158-
}`}
157+
className={`w-full px-3 py-2 bg-[#1E2125] border rounded text-white text-sm focus:outline-none ${errorToDisplay ? 'border-red-400 focus:border-red-400' : 'border-gray-600 focus:border-blue-400'
158+
}`}
159159
placeholder="Enter board title"
160160
disabled={isLoading}
161161
/>
@@ -182,8 +182,7 @@ const CreateBoard: React.FC<CreateBoardProp> = ({ id, workspaceName, onBoardCrea
182182
<button
183183
onClick={handleCreateBoard}
184184
disabled={!boardTitle.trim() || isLoading}
185-
className={`w-full py-2 text-sm rounded font-medium ${
186-
boardTitle.trim() && !isLoading
185+
className={`w-full py-2 text-sm rounded font-medium ${boardTitle.trim() && !isLoading
187186
? 'bg-blue-600 hover:bg-blue-700 text-white'
188187
: 'bg-gray-600 text-gray-400 cursor-not-allowed'
189188
}`}
@@ -195,7 +194,7 @@ const CreateBoard: React.FC<CreateBoardProp> = ({ id, workspaceName, onBoardCrea
195194
</div>
196195
</div>
197196
)}
198-
197+
199198
</>
200199
);
201200
}

frontend/src/pages/workspace/Board.tsx

Lines changed: 117 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import ClosedBoards from "@/components/board/ClosedBoards";
22
import CreateBoard from "@/components/shared/CreateBoard";
3+
import LoadingSpinner from "@/components/ui/LoadingSpinner";
4+
import { importBoard } from "@/services/boardService";
35
import { notify } from "@/services/toastService";
46
import { getBoards, getClosedBoards, getWorkspaceById, updateWorkspace } from "@/services/workspaceService";
57
import type { Board } from "@/types/project";
68
import type { WorkSpace } from "@/types/workspace";
7-
import { Pencil, Users } from "lucide-react";
9+
import { Pencil, Upload, Users } from "lucide-react";
810
import { useEffect, useState, type SetStateAction } from "react";
911
import { 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>

frontend/src/services/axiosClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import axios, { type AxiosInstance, type AxiosResponse } from 'axios';
22

33
const axiosClients: AxiosInstance = axios.create({
44
baseURL:
5-
`${import.meta.env.VITE_API_URL}` ||
5+
import.meta.env.VITE_API_URL ||
66
'http://localhost:9000/api',
77
withCredentials: true,
88
headers: {

frontend/src/services/boardService.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,19 @@ const exportBoard = async (boardId: number): Promise<ApiResponse<null>> => {
7171
return axiosClients.get(`/projects/${boardId}/export`);
7272
};
7373

74+
const importBoard = async (workspaceId: number, file: File): Promise<ApiResponse<Board>> => {
75+
const formData = new FormData();
76+
formData.append('file', file);
77+
return axiosClients.post(`/workspaces/${workspaceId}/projects/import`, formData, {
78+
headers: {
79+
'Content-Type': 'multipart/form-data',
80+
},
81+
});
82+
}
83+
7484
export {
7585
fetchUrlPreview, fetchBoardDetail, createNewColumn,
7686
updateColumn, archiveColumn, restoreColumn,
7787
deleteColumn, updateColumnPosititon, fetchBoardColumns,
78-
fetchArchivedColumns, fetchActiveBoardTasks, fetchBoardMembers, fetchAllBoards, exportBoard,
88+
fetchArchivedColumns, fetchActiveBoardTasks, fetchBoardMembers, fetchAllBoards, exportBoard, importBoard
7989
};

0 commit comments

Comments
 (0)