From 1502edde70d2197408ac516718a1b4f48aa7e4d1 Mon Sep 17 00:00:00 2001 From: subnub Date: Fri, 19 Jul 2024 02:36:38 -0400 Subject: [PATCH] created share modal --- backend/controllers/file.ts | 12 +- backend/db/utils/fileUtils/index.ts | 9 +- backend/express-routers/file.ts | 2 +- backend/services/FileService/index.ts | 6 +- src/api/filesAPI.ts | 15 ++ src/components/ContextMenu/index.tsx | 8 +- src/components/FileInfoPopup/index.tsx | 6 + src/components/MainSection/index.tsx | 7 + src/components/SharePopup/index.tsx | 260 +++++++++++++++++++++++++ src/popups/file.ts | 39 ++++ src/reducers/selected.ts | 22 +++ 11 files changed, 372 insertions(+), 14 deletions(-) create mode 100644 src/components/SharePopup/index.tsx diff --git a/backend/controllers/file.ts b/backend/controllers/file.ts index a25b683..bc9c4b4 100644 --- a/backend/controllers/file.ts +++ b/backend/controllers/file.ts @@ -297,9 +297,9 @@ class FileController { const id = req.params.id; const userID = req.user._id; - await fileService.removeLink(userID, id); + const file = await fileService.removeLink(userID, id); - res.send(); + res.send(file); } catch (e: unknown) { if (e instanceof Error) { console.log("\nRemove Public Link Error File Route:", e.message); @@ -317,9 +317,9 @@ class FileController { const fileID = req.params.id; const userID = req.user._id; - const token = await fileService.makePublic(userID, fileID); + const { file, token } = await fileService.makePublic(userID, fileID); - res.send(token); + res.send({ file, token }); } catch (e: unknown) { if (e instanceof Error) { console.log("\nMake Public Error File Route:", e.message); @@ -353,9 +353,9 @@ class FileController { const id = req.params.id; const userID = req.user._id; - const token = await fileService.makeOneTimePublic(userID, id); + const { file, token } = await fileService.makeOneTimePublic(userID, id); - res.send(token); + res.send({ file, token }); } catch (e: unknown) { if (e instanceof Error) { console.log("\nMake One Time Public Link Error File Route:", e.message); diff --git a/backend/db/utils/fileUtils/index.ts b/backend/db/utils/fileUtils/index.ts index dcbad7d..75db0b1 100644 --- a/backend/db/utils/fileUtils/index.ts +++ b/backend/db/utils/fileUtils/index.ts @@ -26,7 +26,8 @@ class DbUtil { removeLink = async (fileID: string, userID: string) => { const file = await File.findOneAndUpdate( { _id: new ObjectId(fileID), "metadata.owner": userID }, - { $unset: { "metadata.linkType": "", "metadata.link": "" } } + { $unset: { "metadata.linkType": "", "metadata.link": "" } }, + { new: true } ); return file; @@ -35,7 +36,8 @@ class DbUtil { makePublic = async (fileID: string, userID: string, token: string) => { const file = await File.findOneAndUpdate( { _id: new ObjectId(fileID), "metadata.owner": userID }, - { $set: { "metadata.linkType": "public", "metadata.link": token } } + { $set: { "metadata.linkType": "public", "metadata.link": token } }, + { new: true } ); return file; @@ -52,7 +54,8 @@ class DbUtil { makeOneTimePublic = async (fileID: string, userID: string, token: string) => { const file = await File.findOneAndUpdate( { _id: new ObjectId(fileID), "metadata.owner": userID }, - { $set: { "metadata.linkType": "one", "metadata.link": token } } + { $set: { "metadata.linkType": "one", "metadata.link": token } }, + { new: true } ); return file; diff --git a/backend/express-routers/file.ts b/backend/express-routers/file.ts index e064ac4..b2ee3d3 100644 --- a/backend/express-routers/file.ts +++ b/backend/express-routers/file.ts @@ -92,7 +92,7 @@ router.patch("/file-service/restore", auth, fileController.restoreFile); router.patch("/file-service/restore-multi", auth, fileController.restoreMulti); -router.delete("/file-service/remove-link/:id", auth, fileController.removeLink); +router.patch("/file-service/remove-link/:id", auth, fileController.removeLink); router.delete( "/file-service/remove/token-video/:id", diff --git a/backend/services/FileService/index.ts b/backend/services/FileService/index.ts index f923325..4a3db7a 100644 --- a/backend/services/FileService/index.ts +++ b/backend/services/FileService/index.ts @@ -39,6 +39,8 @@ class MongoFileService { const file = await dbUtilsFile.removeLink(fileID, userID); if (!file) throw new NotFoundError("Remove Link File Not Found Error"); + + return file; }; makePublic = async (userID: string, fileID: string) => { @@ -48,7 +50,7 @@ class MongoFileService { if (!file) throw new NotFoundError("Make Public File Not Found Error"); - return token; + return { file, token }; }; getPublicInfo = async (fileID: string, tempToken: string) => { @@ -68,7 +70,7 @@ class MongoFileService { if (!file) throw new NotFoundError("Make One Time Public Not Found Error"); - return token; + return { file, token }; }; getFileInfo = async (userID: string, fileID: string) => { diff --git a/src/api/filesAPI.ts b/src/api/filesAPI.ts index 567886f..bcc5de0 100644 --- a/src/api/filesAPI.ts +++ b/src/api/filesAPI.ts @@ -162,6 +162,21 @@ export const renameFileAPI = async (fileID: string, name: string) => { return response.data; }; +export const makePublicAPI = async (fileID: string) => { + const response = await axios.patch(`/file-service/make-public/${fileID}`); + return response.data; +}; + +export const makeOneTimePublicAPI = async (fileID: string) => { + const response = await axios.patch(`/file-service/make-one/${fileID}`); + return response.data; +}; + +export const removeLinkAPI = async (fileID: string) => { + const response = await axios.patch(`/file-service/remove-link/${fileID}`); + return response.data; +}; + // DELETE export const deleteFileAPI = async (fileID: string) => { const response = await axios.delete(`/file-service/remove`, { diff --git a/src/components/ContextMenu/index.tsx b/src/components/ContextMenu/index.tsx index 48631fe..f73f598 100644 --- a/src/components/ContextMenu/index.tsx +++ b/src/components/ContextMenu/index.tsx @@ -21,7 +21,11 @@ import { } from "../../api/foldersAPI"; import { useClickOutOfBounds, useUtils } from "../../hooks/utils"; import { useAppDispatch } from "../../hooks/store"; -import { resetSelected, setMultiSelectMode } from "../../reducers/selected"; +import { + resetSelected, + setMultiSelectMode, + setShareModal, +} from "../../reducers/selected"; import TrashIcon from "../../icons/TrashIcon"; import MultiSelectIcon from "../../icons/MultiSelectIcon"; import RenameIcon from "../../icons/RenameIcon"; @@ -195,7 +199,7 @@ const ContextMenu: React.FC = memo((props) => { const openShareItemModal = () => { props.closeContext(); - dispatch(setShareSelected(props.file)); + dispatch(setShareModal(props.file!)); }; const downloadItem = () => { diff --git a/src/components/FileInfoPopup/index.tsx b/src/components/FileInfoPopup/index.tsx index 56f4888..4cdf093 100644 --- a/src/components/FileInfoPopup/index.tsx +++ b/src/components/FileInfoPopup/index.tsx @@ -49,10 +49,16 @@ const FileInfoPopup = memo(() => { downloadFileAPI(file._id); }; + const outterWrapperClick = (e: any) => { + if (e.target.id !== "outer-wrapper" || contextMenuState.selected) return; + dispatch(resetPopupSelect()); + }; + return (
{contextMenuState.selected && (
diff --git a/src/components/MainSection/index.tsx b/src/components/MainSection/index.tsx index 5947388..427b583 100644 --- a/src/components/MainSection/index.tsx +++ b/src/components/MainSection/index.tsx @@ -10,9 +10,14 @@ import Medias from "../Medias"; import { useAppSelector } from "../../hooks/store"; import PhotoViewerPopup from "../PhotoViewerPopup"; import FileInfoPopup from "../FileInfoPopup"; +import SharePopup from "../SharePopup"; const MainSection = memo(() => { const selectedItem = useAppSelector((state) => state.selected.popupModal); + const shareModalItem = useAppSelector( + (state) => state.selected.shareModal.file + ); + const { isMedia } = useUtils(); return (
@@ -30,6 +35,8 @@ const MainSection = memo(() => { ) : undefined} + {shareModalItem && } +
diff --git a/src/components/SharePopup/index.tsx b/src/components/SharePopup/index.tsx new file mode 100644 index 0000000..144dd08 --- /dev/null +++ b/src/components/SharePopup/index.tsx @@ -0,0 +1,260 @@ +import { memo, useEffect, useMemo, useState } from "react"; +import CloseIcon from "../../icons/CloseIcon"; +import { useAppDispatch, useAppSelector } from "../../hooks/store"; +import { getFileColor, getFileExtension } from "../../utils/files"; +import ActionsIcon from "../../icons/ActionsIcon"; +import bytes from "bytes"; +import moment from "moment"; +import { makePublicPopup, removeLinkPopup } from "../../popups/file"; +import { + makeOneTimePublicAPI, + makePublicAPI, + removeLinkAPI, +} from "../../api/filesAPI"; +import { useFilesClient } from "../../hooks/files"; +import { + resetShareModal, + setMainSelect, + setShareModal, +} from "../../reducers/selected"; +import SpinnerPage from "../SpinnerPage"; + +const SharePopup = memo(() => { + const file = useAppSelector((state) => state.selected.shareModal.file)!; + const [updating, setUpdating] = useState(false); + const [shareLink, setShareLink] = useState(""); + const dispatch = useAppDispatch(); + const { invalidateFilesCache } = useFilesClient(); + + const imageColor = useMemo( + () => getFileColor(file.filename), + [file.filename] + ); + + const fileExtension = useMemo( + () => getFileExtension(file.filename, 3), + [file.filename] + ); + + const formattedDate = useMemo(() => { + return moment(file.uploadDate).format("MM/DD/YYYY"); + }, [file.uploadDate, moment]); + + const fileSize = useMemo(() => { + return bytes(file.length); + }, [file.length, bytes]); + + const makePublic = async () => { + try { + const result = await makePublicPopup(); + if (!result) return; + setUpdating(true); + const { file: updatedFile } = await makePublicAPI(file._id); + dispatch( + setMainSelect({ + file: updatedFile, + id: updatedFile._id, + type: "file", + folder: null, + }) + ); + dispatch(setShareModal(updatedFile)); + invalidateFilesCache(); + } catch (e) { + console.log("Error making file public", e); + } + setUpdating(false); + }; + + const makeOneTimePublic = async () => { + try { + const result = await makePublicPopup(); + if (!result) return; + setUpdating(true); + const { file: updatedFile } = await makeOneTimePublicAPI(file._id); + dispatch( + setMainSelect({ + file: updatedFile, + id: updatedFile._id, + type: "file", + folder: null, + }) + ); + dispatch(setShareModal(updatedFile)); + invalidateFilesCache(); + } catch (e) { + console.log("Error making file public", e); + } + setUpdating(false); + }; + + const removeLink = async () => { + try { + const result = await removeLinkPopup(); + if (!result) return; + setUpdating(true); + const updatedFile = await removeLinkAPI(file._id); + dispatch( + setMainSelect({ + file: updatedFile, + id: updatedFile._id, + type: "file", + folder: null, + }) + ); + dispatch(setShareModal(updatedFile)); + invalidateFilesCache(); + setShareLink(""); + } catch (e) { + console.log("Error removing link", e); + } + setUpdating(false); + }; + + const copyLink = () => { + navigator.clipboard.writeText(shareLink); + }; + + const closeShareModal = () => { + dispatch(resetShareModal()); + }; + + const outterWrapperClick = (e: any) => { + if (e.target.id !== "outer-wrapper") return; + closeShareModal(); + }; + + useEffect(() => { + if (!file.metadata.link) return; + const url = `${window.location.origin}/download-page/${file._id}/${shareLink}`; + console.log("url", url); + setShareLink(url); + }, [file.metadata.link]); + + return ( +
+
+
+ +
+ + {fileExtension} + +
+
+

+ {file.filename} +

+
+
+
+ +
+
+
+
+

Share file

+
+ + Type + + + {fileExtension} + +
+
+ + Size + + + {fileSize} + +
+
+ + Created + + + {formattedDate} + +
+
+ + Access + + + {file.metadata.link ? "Public" : "Private"} + +
+ + {shareLink && ( +
+ + +
+ )} + + {!shareLink && ( +
+ + +
+ )} + + {shareLink && ( +
+ +
+ )} + + {updating && ( +
+ +
+ )} +
+
+ ); +}); + +export default SharePopup; diff --git a/src/popups/file.ts b/src/popups/file.ts index 2451a69..513b2a3 100644 --- a/src/popups/file.ts +++ b/src/popups/file.ts @@ -49,3 +49,42 @@ export const trashItemsPopup = async () => { }); return result.value; }; + +export const makePublicPopup = async () => { + const result = await Swal.fire({ + title: "Make file public?", + text: "This iwill make the file public, anyone with the link will be able to access the file.", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + confirmButtonText: "Yes", + }); + return result.value; +}; + +export const makeOneTimePublicPopup = async () => { + const result = await Swal.fire({ + title: "Make file public?", + text: "This iwill make the file public, anyone with the link will be able to access the file for a single time.", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + confirmButtonText: "Yes", + }); + return result.value; +}; + +export const removeLinkPopup = async () => { + const result = await Swal.fire({ + title: "Remove link?", + text: "This will remove public access to the file.", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + confirmButtonText: "Yes", + }); + return result.value; +}; diff --git a/src/reducers/selected.ts b/src/reducers/selected.ts index 6492e10..dc17281 100644 --- a/src/reducers/selected.ts +++ b/src/reducers/selected.ts @@ -20,6 +20,9 @@ export interface SelectedStateType { [key: string]: MainSecionType; }; multiSelectCount: number; + shareModal: { + file: FileInterface | null; + }; } const initialState: SelectedStateType = { @@ -36,6 +39,9 @@ const initialState: SelectedStateType = { multiSelectMode: false, multiSelectMap: {}, multiSelectCount: 0, + shareModal: { + file: null, + }, }; const selectedSlice = createSlice({ @@ -90,6 +96,20 @@ const selectedSlice = createSlice({ file: null, }; }, + setShareModal: (state, action: PayloadAction) => { + state.popupModal = { + type: "", + file: null, + }; + state.shareModal = { + file: action.payload, + }; + }, + resetShareModal: (state) => { + state.shareModal = { + file: null, + }; + }, }, }); @@ -100,6 +120,8 @@ export const { resetMultiSelect, setPopupSelect, resetPopupSelect, + setShareModal, + resetShareModal, } = selectedSlice.actions; export default selectedSlice.reducer;