(REFACTOR): Moved all the code around and organized it.
It was a mess in here, holy shit.
This commit is contained in:
parent
1dd9f4e38f
commit
d8873e08f2
@ -8,6 +8,7 @@
|
||||
- [ ] Image display (will require some kind of file server)
|
||||
- [ ] Markdown rendering (quite a reach)
|
||||
- [x] Mobile friendly
|
||||
- [ ] Fix selection bug
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@ -11,46 +11,66 @@ import Uploader from "../components/Uploader.jsx";
|
||||
import CreateDirectory from "../components/CreateDirectory.jsx";
|
||||
|
||||
export default function Dashboard() {
|
||||
// Store the default path
|
||||
// TODO: BACK TO NORMAL PATH
|
||||
const defaultPath = ["media", "vault"];
|
||||
// const defaultPath = ["home", "azpect"];
|
||||
// ---- CONSTANTS ---- //
|
||||
/**
|
||||
* Default path
|
||||
* Uses the .env var in local development, but when
|
||||
* pushed to dockerhub, the .env is ignored and the
|
||||
* default path is used.
|
||||
*/
|
||||
const defaultPath = (import.meta.env.VITE_DEFAULT_PATH || "media,vault").split(',');
|
||||
|
||||
/**
|
||||
* URL To the backend web server.
|
||||
* Uses the .env var in local development, but when
|
||||
* pushed to dockerhub, the .env is ignored and the real
|
||||
* backend URL is used.
|
||||
* @type {string}
|
||||
* pushed to dockerhub, the .env is ignored and the
|
||||
* real backend URL is used.
|
||||
*/
|
||||
const backendUrl = import.meta.env.VITE_BACKEND_URL || "https://backend.gophernest.net";
|
||||
|
||||
/**
|
||||
* The name of the value stored in local storage.
|
||||
*/
|
||||
const storage_id = "gophernest_credentials";
|
||||
|
||||
|
||||
// ---- STATE ---- //
|
||||
|
||||
// General state
|
||||
const [token, setToken] = useState(null);
|
||||
const [path, setPath] = useState([...defaultPath]);
|
||||
const [files, setFiles] = useState([]);
|
||||
|
||||
// User settings
|
||||
const [showHidden, setShowHidden] = useState(false);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [files, setFiles] = useState([]);
|
||||
|
||||
// Modals
|
||||
const [error, setError] = useState(null);
|
||||
const [editing, setEditing] = useState("");
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
// Loading spinners
|
||||
const [childrenLoading, setChildrenLoading] = useState(false);
|
||||
const [downloadLoading, setDownloadLoading] = useState(false);
|
||||
const [contentLoading, setContentLoading] = useState(false);
|
||||
|
||||
|
||||
// Handle the state for the content being modified in the text editor.
|
||||
const [editingFileContent, setEditingFileContent] = useState("");
|
||||
|
||||
// Other
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
/**
|
||||
* The name of the value stored in local storage.
|
||||
* @type {string}
|
||||
*/
|
||||
const storage_id = "gophernest_credentials";
|
||||
// ---- FUNCTIONS ---- //
|
||||
|
||||
/**
|
||||
* Update the token from the local or session storage.
|
||||
* This function assumes one of them exists.
|
||||
* If it does not, the token will be null.
|
||||
* This function will return the token as well, to allow for other usecases.
|
||||
* @returns {string | null} Token from browser storage
|
||||
*/
|
||||
const updateToken = () => {
|
||||
const t = localStorage.getItem(storage_id) ? localStorage.getItem(storage_id) : sessionStorage.getItem(storage_id);
|
||||
@ -58,6 +78,166 @@ export default function Dashboard() {
|
||||
return t;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the error in the error state.
|
||||
*/
|
||||
const clearError = () => setError(null);
|
||||
|
||||
/**
|
||||
* Toggle editing of a file.
|
||||
* @param path {string}
|
||||
*/
|
||||
const toggleEditing = (path) => setEditing(path);
|
||||
|
||||
/**
|
||||
* Close the file that is being edited.
|
||||
*/
|
||||
const exitFile = () => setEditing("");
|
||||
|
||||
/**
|
||||
* Show the hidden files
|
||||
*/
|
||||
const toggleHidden = (e) => setShowHidden(e.target.checked);
|
||||
|
||||
/**
|
||||
* Toggle the upload modal.
|
||||
* This can be used in the navbar and the close button!
|
||||
*/
|
||||
const toggleUploading = () => setUploading(!uploading);
|
||||
|
||||
/**
|
||||
* Show the creating modal.
|
||||
*/
|
||||
const createDir = () => setCreating(true);
|
||||
|
||||
/**
|
||||
* Hide the creating modal.
|
||||
*/
|
||||
const closeCreate = () => setCreating(false);
|
||||
|
||||
// ---- HANDLERS --- //
|
||||
|
||||
/**
|
||||
* Updates the path by slicing [0:index]
|
||||
* @param index {number} - Index to slice to.
|
||||
*/
|
||||
const updatePath = (index) => {
|
||||
let newPath = path.slice(0, index + 1);
|
||||
if (newPath.length < defaultPath.length) {
|
||||
newPath = [...defaultPath];
|
||||
}
|
||||
setPath(newPath);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the path back to the default.
|
||||
* TODO: Fix this in production? Not sure why
|
||||
*/
|
||||
const backHome = () => setPath([...defaultPath]);
|
||||
|
||||
/**
|
||||
* Add name to the path.
|
||||
* @param name {string} - Target child
|
||||
*/
|
||||
const appendPath = (name) => setPath([...path, name])
|
||||
|
||||
/**
|
||||
* Back arrow, goes back one directory (cd ..)
|
||||
*/
|
||||
const backArrow = () => {
|
||||
if (path.length > defaultPath.length) {
|
||||
setPath(path.slice(0, path.length - 1));
|
||||
}
|
||||
}
|
||||
|
||||
const exitAndSaveFile = (newContent) => {
|
||||
// Send request to server to update the file. This will return nothing
|
||||
// so no need for any promise handling.
|
||||
updateContent(editing, newContent).then((data) => {
|
||||
if (data.code === 200) {
|
||||
setEditing("");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This isn't fast, but hopefully the use case will be small batches.
|
||||
* @param file {string} - The file to toggle
|
||||
*/
|
||||
const toggleSelected = (file) => {
|
||||
if (!selected.includes(file)) {
|
||||
setSelected([...selected, file]);
|
||||
} else {
|
||||
const idx = selected.indexOf(file);
|
||||
setSelected([...selected.slice(0, idx), ...selected.slice(idx + 1, selected.length)])
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback function for when the download button is clicked.
|
||||
*/
|
||||
const downloadFiles = () => {
|
||||
// Do not allow empty downloads
|
||||
if (selected.length === 0) {
|
||||
setError("Please select files/directories to download.");
|
||||
return
|
||||
}
|
||||
|
||||
setDownloadLoading(true);
|
||||
|
||||
const targets = selected.map((file) => `/${path.join("/")}/${file}`);
|
||||
|
||||
download(targets).catch((err) => {
|
||||
setError(`Download error: ${err}.`)
|
||||
}).finally(() => {
|
||||
setDownloadLoading(false)
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a directory or file in the backend
|
||||
* @param name {string} Name of new file or directory
|
||||
*/
|
||||
const createDirectory = (name) => {
|
||||
create(name, path).then((data) => {
|
||||
if (data.code === 201) {
|
||||
setCreating(false);
|
||||
}
|
||||
}).catch((error) => {
|
||||
setError(error);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the selected files.
|
||||
*/
|
||||
const removeSelected = () => {
|
||||
if (selected.length === 0) {
|
||||
setError("Please select files or directories to delete");
|
||||
return;
|
||||
}
|
||||
|
||||
// Files are stored as arrays of paths
|
||||
const files = selected.map((file) => [...path, file]);
|
||||
|
||||
remove(files).then((data) => {
|
||||
if (data.code === 201) {
|
||||
console.log(data);
|
||||
setSelected([]);
|
||||
|
||||
// Fetch the new files & deselect everything
|
||||
fetchFiles();
|
||||
}
|
||||
}).catch((error) => {
|
||||
setError(error);
|
||||
});
|
||||
};
|
||||
|
||||
// ---- SERVER FUNCTIONS ---- //
|
||||
|
||||
/**
|
||||
* Fetch a list of files in the current directory.
|
||||
*/
|
||||
const fetchFiles = () => {
|
||||
const getData = async (token) => {
|
||||
const response = await fetch(`${backendUrl}/v1/children?path=/${path.join("/")}`, {
|
||||
@ -92,81 +272,33 @@ export default function Dashboard() {
|
||||
setSelected([]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchFiles();
|
||||
}, [path, uploading, creating]);
|
||||
|
||||
|
||||
// Redirect if the user isn't logged in, otherwise update the state.
|
||||
// Store the token in the storage, it should be attached to every request.
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem(storage_id) == null && sessionStorage.getItem(storage_id) == null) {
|
||||
navigate("/login");
|
||||
} else {
|
||||
updateToken();
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
/**
|
||||
* Updates the path by slicing [0:index]
|
||||
* @param index {number} Index to slice to.
|
||||
* Update the contents of a file.
|
||||
* @param path {string} - Path to the file.
|
||||
* @param content {string} - New content to add to the file.
|
||||
* @return {any} - Server response, nothing.
|
||||
*/
|
||||
const updatePath = (index) => {
|
||||
let newPath = path.slice(0, index + 1);
|
||||
if (newPath.length < defaultPath.length) {
|
||||
newPath = [...defaultPath];
|
||||
}
|
||||
setPath(newPath);
|
||||
const updateContent = async (path, content) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/update`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ path, content }),
|
||||
})
|
||||
|
||||
if (!resp.ok) setError("An error occurred when saving the file. Please try again.");
|
||||
|
||||
const json = resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the path back to the default.
|
||||
* Download a list of files.
|
||||
* @param paths {string[]} - List of paths to download.
|
||||
* @returns void
|
||||
*/
|
||||
const backHome = () => {
|
||||
// TODO: Fix this in production
|
||||
setPath([...defaultPath]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add name to the path.
|
||||
* @param name Target child
|
||||
*/
|
||||
const appendPath = (name) => {
|
||||
setPath([...path, name])
|
||||
};
|
||||
|
||||
/**
|
||||
* Back arrow, goes back one directory (cd ..)
|
||||
*/
|
||||
const backArrow = () => {
|
||||
if (path.length > defaultPath.length) {
|
||||
setPath(path.slice(0, path.length - 1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This isn't fast, but hopefully the use case will be small batches.
|
||||
* @param file {string} The file to toggle
|
||||
*/
|
||||
const toggleSelected = (file) => {
|
||||
if (!selected.includes(file)) {
|
||||
setSelected([...selected, file]);
|
||||
} else {
|
||||
const idx = selected.indexOf(file);
|
||||
setSelected([...selected.slice(0, idx), ...selected.slice(idx + 1, selected.length)])
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback function for when the download button is clicked.
|
||||
*/
|
||||
const downloadFiles = () => {
|
||||
// Do not allow empty downloads
|
||||
if (selected.length === 0) {
|
||||
setError("Please select files/directories to download.");
|
||||
return
|
||||
}
|
||||
|
||||
const download = async (paths) => {
|
||||
try {
|
||||
const resp = await fetch(`${backendUrl}/v1/download`, {
|
||||
@ -197,70 +329,11 @@ export default function Dashboard() {
|
||||
}
|
||||
};
|
||||
|
||||
setDownloadLoading(true);
|
||||
|
||||
const targets = [];
|
||||
selected.forEach((file) => {
|
||||
targets.push(`/${path.join("/")}/${file}`);
|
||||
});
|
||||
|
||||
// TODO: Implement UI for errors
|
||||
download(targets).catch((err) => {
|
||||
setError(`Download error: ${err}.`)
|
||||
}).finally(() => {
|
||||
setDownloadLoading(false)
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the error in the error state.
|
||||
* Fetch the content of a file.
|
||||
* @param path {string} - File content.
|
||||
* @returns {any} - Server response with content.
|
||||
*/
|
||||
const clearError = () => {
|
||||
setError(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle editing of a file.
|
||||
* @param path {string}
|
||||
*/
|
||||
const toggleEditing = (path) => {
|
||||
setEditing(path);
|
||||
};
|
||||
|
||||
const exitFile = () => {
|
||||
setEditing("");
|
||||
};
|
||||
|
||||
const exitAndSaveFile = (newContent) => {
|
||||
const updateContent = async (path, content) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/update`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ path, content }),
|
||||
})
|
||||
if (!resp.ok) {
|
||||
setError("An error occurred when saving the file. Please try again.");
|
||||
}
|
||||
return await resp.json();
|
||||
};
|
||||
|
||||
// Send request to server to update the file. This will return nothing
|
||||
// so no need for any promise handling.
|
||||
updateContent(editing, newContent).then((data) => {
|
||||
if (data.code === 200) {
|
||||
setEditing("");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the state for the content being modified in the text editor.
|
||||
*/
|
||||
const [editingFileContent, setEditingFileContent] = useState("");
|
||||
useEffect(() => {
|
||||
const fetchContent = async (path) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/content?path=${path}`, {
|
||||
method: "GET",
|
||||
@ -269,14 +342,85 @@ export default function Dashboard() {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!resp.ok) setError("Something went wrong! Failed to get file content.")
|
||||
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a file or directory.
|
||||
* @param name {string} - Name of the file or directory.
|
||||
* @param cwd {string[]} - Current directory as a list of paths.
|
||||
* @return {any} Server response.
|
||||
*/
|
||||
const create = async (name, cwd) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/create`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ name, cwd })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
// TODO: Add this back in, its broken right now.
|
||||
setError("Something went wrong! Failed to get file content.")
|
||||
const data = await resp.json()
|
||||
setError(data.error);
|
||||
return data;
|
||||
}
|
||||
|
||||
const json = resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a list of files.
|
||||
* @param files {string[][]} - List of file paths as list of sub-paths.
|
||||
* @returns {any} Server response.
|
||||
*/
|
||||
const remove = async (files) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/delete`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ files, root: defaultPath })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json()
|
||||
setError(data.error);
|
||||
return data;
|
||||
}
|
||||
|
||||
return await resp.json();
|
||||
};
|
||||
|
||||
// ---- EFFECTS ---- //
|
||||
|
||||
// Update the file list from the server each time an action requires
|
||||
// an update.
|
||||
useEffect(() => {
|
||||
fetchFiles();
|
||||
}, [path, uploading, creating]);
|
||||
|
||||
|
||||
// Redirect if the user isn't logged in, otherwise update the state.
|
||||
// Store the token in the storage, it should be attached to every request.
|
||||
useEffect(() => {
|
||||
(localStorage.getItem(storage_id) == null && sessionStorage.getItem(storage_id) == null)
|
||||
? navigate("/login")
|
||||
: updateToken();
|
||||
}, [navigate]);
|
||||
|
||||
// Load the content from the file into the editor and allow the user to begin
|
||||
// editing the file.
|
||||
useEffect(() => {
|
||||
setContentLoading(true);
|
||||
|
||||
// Prevent running when nothing is being edited. Also prevents a call on mount.
|
||||
if (editing) {
|
||||
// Fetch the data and handle errors accordingly
|
||||
@ -295,17 +439,6 @@ export default function Dashboard() {
|
||||
|
||||
}, [editing]);
|
||||
|
||||
const toggleHidden = (e) => {
|
||||
setShowHidden(e.target.checked);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the upload modal.
|
||||
* This can be used in the navbar and the close button!
|
||||
*/
|
||||
const toggleUploading = () => {
|
||||
setUploading(!uploading);
|
||||
};
|
||||
|
||||
/**
|
||||
* This will be where the magic happens, where the files are upload
|
||||
@ -351,93 +484,6 @@ export default function Dashboard() {
|
||||
});
|
||||
};
|
||||
|
||||
const createDir = () => {
|
||||
setCreating(true);
|
||||
};
|
||||
|
||||
const closeCreate = () => {
|
||||
setCreating(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a directory or file in the backend
|
||||
* @param name {string} Name of new directory/file
|
||||
*/
|
||||
const createDirectory = (name) => {
|
||||
const create = async (name, cwd) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/create`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ name, cwd })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json()
|
||||
setError(data.error);
|
||||
return data;
|
||||
}
|
||||
|
||||
return await resp.json();
|
||||
};
|
||||
|
||||
create(name, path).then((data) => {
|
||||
if (data.code === 201) {
|
||||
setCreating(false);
|
||||
}
|
||||
}).catch((error) => {
|
||||
setError(error);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the selected files.
|
||||
*/
|
||||
const removeSelected = () => {
|
||||
if (selected.length === 0) {
|
||||
return setError("Please select files or directories to delete");
|
||||
}
|
||||
|
||||
const remove = async (files) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/delete`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ files, root: defaultPath })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json()
|
||||
setError(data.error);
|
||||
return data;
|
||||
}
|
||||
|
||||
return await resp.json();
|
||||
};
|
||||
|
||||
// Files are stored as arrays of paths
|
||||
const files = [];
|
||||
for (const file of selected) {
|
||||
files.push([...path, file]);
|
||||
}
|
||||
|
||||
remove(files).then((data) => {
|
||||
if (data.code === 201) {
|
||||
console.log(data);
|
||||
setSelected([]);
|
||||
|
||||
// Fetch the new files & deselect everything
|
||||
fetchFiles();
|
||||
}
|
||||
}).catch((error) => {
|
||||
setError(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen h-screen pb-8">
|
||||
<Navbar downloadFiles={downloadFiles} uploadFiles={toggleUploading} />
|
||||
|
||||
Reference in New Issue
Block a user