mirror of
https://github.com/20kaushik02/spotify-manager-web.git
synced 2025-12-06 09:54:07 +00:00
import/export data, nav layout change, auth state improvement, css corrections and improvements
This commit is contained in:
parent
098706a70e
commit
e5e0751e8f
@ -19,13 +19,9 @@
|
|||||||
sizes="16x16"
|
sizes="16x16"
|
||||||
href="%PUBLIC_URL%/favicon-16x16.png"
|
href="%PUBLIC_URL%/favicon-16x16.png"
|
||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#443c9c" />
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta
|
<meta name="description" content="Frontend for spotify-manager" />
|
||||||
name="description"
|
|
||||||
content="Web site created using create-react-app"
|
|
||||||
/>
|
|
||||||
<!--
|
<!--
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
@ -40,7 +36,7 @@
|
|||||||
work correctly both with client-side routing and a non-root public URL.
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<title>React App</title>
|
<title>Spotify Manager</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "React App",
|
"short_name": "Spotify Manager",
|
||||||
"name": "Create React App Sample",
|
"name": "Spotify Manager",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "android-chrome-192x192.png",
|
"src": "android-chrome-192x192.png",
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export const RefreshAuthContext: Context<() => Promise<boolean>> =
|
|||||||
function App(): React.ReactNode {
|
function App(): React.ReactNode {
|
||||||
// States
|
// States
|
||||||
const [width, setWidth] = useState(0);
|
const [width, setWidth] = useState(0);
|
||||||
const [auth, setAuth] = useState(false);
|
const [auth, setAuth] = useState(true);
|
||||||
|
|
||||||
const refreshAuth = async () => {
|
const refreshAuth = async () => {
|
||||||
// reauth
|
// reauth
|
||||||
@ -82,7 +82,9 @@ function App(): React.ReactNode {
|
|||||||
showErrorToastNotification(resp.data.message);
|
showErrorToastNotification(resp.data.message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (resp.status === 401) await refreshAuth();
|
if (resp.status === 401) {
|
||||||
|
return await refreshAuth();
|
||||||
|
}
|
||||||
setAuth(false);
|
setAuth(false);
|
||||||
showWarnToastNotification(resp.data.message);
|
showWarnToastNotification(resp.data.message);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
20
src/api/load.ts
Normal file
20
src/api/load.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
import { type apiRespBaseType, axiosInstance } from "./axiosInstance.ts";
|
||||||
|
import { loadImportDataURL } from "./paths.ts";
|
||||||
|
|
||||||
|
interface importGraphDataType extends apiRespBaseType {}
|
||||||
|
|
||||||
|
export const apiImportGraph = async (
|
||||||
|
data: File
|
||||||
|
): Promise<AxiosResponse<importGraphDataType, any>> => {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("dataFile", data);
|
||||||
|
const response = await axiosInstance.put(loadImportDataURL, formData, {
|
||||||
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -18,3 +18,7 @@ export const opBackfillLinkURL = "api/operations/populate/link";
|
|||||||
export const opBackfillChainURL = "api/operations/populate/chain";
|
export const opBackfillChainURL = "api/operations/populate/chain";
|
||||||
export const opPruneLinkURL = "api/operations/prune/link";
|
export const opPruneLinkURL = "api/operations/prune/link";
|
||||||
export const opPruneChainURL = "api/operations/prune/chain";
|
export const opPruneChainURL = "api/operations/prune/chain";
|
||||||
|
|
||||||
|
export const loadExportDataURL = "api/load";
|
||||||
|
export const loadExportDataFullURL: string = backendDomain + loadExportDataURL;
|
||||||
|
export const loadImportDataURL = "api/load";
|
||||||
|
|||||||
@ -10,12 +10,11 @@ const Navbar = (): React.ReactNode => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={`${styles.navbar_wrapper} custom_scrollbar`}>
|
<nav className={`${styles.navbar_wrapper} custom_scrollbar`}>
|
||||||
<StyledNavLink path="/" text="About" />
|
<StyledNavLink path="/" text="Home" />
|
||||||
<StyledNavLink path="/graph" text="Graph" />
|
<StyledNavLink path="/graph" text="Graph" />
|
||||||
<StyledNavLink path="/how-to" text="How To" />
|
<StyledNavLink path="/how-to" text="How To" />
|
||||||
<StyledNavLink path="/settings" text="Settings" />
|
|
||||||
{auth === true ? (
|
{auth === true ? (
|
||||||
<StyledNavLink path="/logout" text="Logout" />
|
<StyledNavLink path="/settings" text="Settings" />
|
||||||
) : (
|
) : (
|
||||||
<StyledNavLink path="/login" text="Login" />
|
<StyledNavLink path="/login" text="Login" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
/* Font Size */
|
/* Font Size */
|
||||||
:root {
|
:root {
|
||||||
--normalFontSize: 16px;
|
--normalFontSize: 16px;
|
||||||
--headingFontSize: 24px;
|
--headingFontSize: 48px;
|
||||||
--headingFont: AileronFont;
|
--headingFont: AileronFont;
|
||||||
--primaryFont: AileronFont;
|
--primaryFont: AileronFont;
|
||||||
--text: whitesmoke;
|
--text: whitesmoke;
|
||||||
@ -66,6 +66,10 @@ ul {
|
|||||||
list-style: outside;
|
list-style: outside;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: var(--headingFontSize);
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
@ -106,3 +110,11 @@ code {
|
|||||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||||
background-color: var(--bg);
|
background-color: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
display: block;
|
||||||
|
height: 1px;
|
||||||
|
width: 100%;
|
||||||
|
margin: var(--mb-2) auto;
|
||||||
|
border-top: 1px solid white;
|
||||||
|
}
|
||||||
|
|||||||
@ -30,11 +30,3 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
|
||||||
display: block;
|
|
||||||
height: 1px;
|
|
||||||
width: 100%;
|
|
||||||
margin: var(--mb-2) auto;
|
|
||||||
border-top: 1px solid white;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -249,7 +249,10 @@ const Graph = (): React.ReactNode => {
|
|||||||
showErrorToastNotification("Can't delete playlists!");
|
showErrorToastNotification("Can't delete playlists!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!edges[0]) throw new ReferenceError("no edge selected");
|
if (!edges[0]) {
|
||||||
|
showWarnToastNotification("Select a link!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
console.debug(
|
console.debug(
|
||||||
`deleted connection: ${edges[0].source} -> ${edges[0].target}`
|
`deleted connection: ${edges[0].source} -> ${edges[0].target}`
|
||||||
);
|
);
|
||||||
@ -598,7 +601,7 @@ const Graph = (): React.ReactNode => {
|
|||||||
</span>
|
</span>
|
||||||
Backfill Chain
|
Backfill Chain
|
||||||
</Button>
|
</Button>
|
||||||
<hr className={styles.divider} />
|
<hr className="divider" />
|
||||||
<Button disabled={loading} onClickMethod={pruneLink}>
|
<Button disabled={loading} onClickMethod={pruneLink}>
|
||||||
<IoArrowDownOutline size={36} />
|
<IoArrowDownOutline size={36} />
|
||||||
Prune Link
|
Prune Link
|
||||||
@ -610,7 +613,7 @@ const Graph = (): React.ReactNode => {
|
|||||||
</span>
|
</span>
|
||||||
Prune Chain
|
Prune Chain
|
||||||
</Button>
|
</Button>
|
||||||
<hr className={styles.divider} />
|
<hr className="divider" />
|
||||||
<Button disabled={loading} onClickMethod={() => arrangeLayout("TB")}>
|
<Button disabled={loading} onClickMethod={() => arrangeLayout("TB")}>
|
||||||
<IoIosGitNetwork size={36} />
|
<IoIosGitNetwork size={36} />
|
||||||
Arrange
|
Arrange
|
||||||
@ -623,7 +626,7 @@ const Graph = (): React.ReactNode => {
|
|||||||
)}
|
)}
|
||||||
{isInteractive() ? "Lock" : "Unlock"}
|
{isInteractive() ? "Lock" : "Unlock"}
|
||||||
</Button>
|
</Button>
|
||||||
<hr className={styles.divider} />
|
<hr className="divider" />
|
||||||
<Button disabled={loading} onClickMethod={updateUserData}>
|
<Button disabled={loading} onClickMethod={updateUserData}>
|
||||||
<span className={styles.icons}>
|
<span className={styles.icons}>
|
||||||
<WiCloudRefresh size={36} />
|
<WiCloudRefresh size={36} />
|
||||||
|
|||||||
@ -3,5 +3,4 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 10vw;
|
margin-right: 10vw;
|
||||||
font-size: var(--headingFontSize);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 10vw;
|
margin-right: 10vw;
|
||||||
font-size: var(--headingFontSize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app_logo {
|
.app_logo {
|
||||||
|
|||||||
@ -3,5 +3,4 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 10vw;
|
margin-right: 10vw;
|
||||||
font-size: var(--headingFontSize);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,11 @@ const Login = ():React.ReactNode => {
|
|||||||
|
|
||||||
return () => clearTimeout(timeoutID);
|
return () => clearTimeout(timeoutID);
|
||||||
}, []);
|
}, []);
|
||||||
return <div className={styles.login_wrapper}>Logging in to Spotify...</div>;
|
return (
|
||||||
|
<div className={styles.login_wrapper}>
|
||||||
|
<h1>Logging in to Spotify...</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Login;
|
export default Login;
|
||||||
|
|||||||
@ -3,5 +3,4 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 10vw;
|
margin-right: 10vw;
|
||||||
font-size: var(--headingFontSize);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,11 @@ const Logout = ():React.ReactNode => {
|
|||||||
|
|
||||||
return () => clearTimeout(timeoutID);
|
return () => clearTimeout(timeoutID);
|
||||||
}, []);
|
}, []);
|
||||||
return <div className={styles.logout_wrapper}>See you soon!</div>;
|
return (
|
||||||
|
<div className={styles.logout_wrapper}>
|
||||||
|
<h1>See you soon!</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Logout;
|
export default Logout;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.pnf_wrapper {
|
.pnf_wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 10vw;
|
margin-right: 10vw;
|
||||||
font-size: var(--headingFontSize);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,19 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import styles from "./PageNotFound.module.css";
|
import styles from "./PageNotFound.module.css";
|
||||||
import { showWarnToastNotification } from "../../components/ToastNotification/index.tsx";
|
import { showWarnToastNotification } from "../../components/ToastNotification/index.tsx";
|
||||||
|
import AnimatedSVG from "../../components/AnimatedSVG/index.tsx";
|
||||||
|
|
||||||
const PageNotFound = (): React.ReactNode => {
|
const PageNotFound = (): React.ReactNode => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
showWarnToastNotification("Oops!");
|
showWarnToastNotification("Oops!");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <div className={styles.pnf_wrapper}>Page Not Found</div>;
|
return (
|
||||||
|
<div className={styles.pnf_wrapper}>
|
||||||
|
<AnimatedSVG/>
|
||||||
|
<h1>Page Not Found</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PageNotFound;
|
export default PageNotFound;
|
||||||
|
|||||||
@ -4,10 +4,45 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 10vw;
|
margin-right: 10vw;
|
||||||
font-size: var(--headingFontSize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings_controls {
|
.settings_controls {
|
||||||
padding: var(--mb-4);
|
width: 20vw;
|
||||||
|
gap: var(--mb-2);
|
||||||
|
padding: var(--mb-2);
|
||||||
background-color: var(--bgNav);
|
background-color: var(--bgNav);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings_dataFile {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
.settings_dataFile::-webkit-file-upload-button {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.settings_dataFile::before {
|
||||||
|
content: "Upload";
|
||||||
|
color: var(--text);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bg);
|
||||||
|
/* border: 1px solid #999; */
|
||||||
|
padding: var(--mb-2) 0;
|
||||||
|
outline: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.settings_dataFile:hover::before {
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings_dataFile_selection {
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,82 @@
|
|||||||
import React from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import styles from "./Settings.module.css";
|
import styles from "./Settings.module.css";
|
||||||
import Button from "../../components/Button/index.tsx";
|
import Button from "../../components/Button/index.tsx";
|
||||||
|
import { loadExportDataFullURL } from "../../api/paths.ts";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
showSuccessToastNotification,
|
||||||
|
showWarnToastNotification,
|
||||||
|
} from "../../components/ToastNotification/index.tsx";
|
||||||
|
import APIWrapper from "../../components/APIWrapper/index.tsx";
|
||||||
|
import { apiImportGraph } from "../../api/load.ts";
|
||||||
|
import { RefreshAuthContext } from "../../App.tsx";
|
||||||
|
import SimpleLoader from "../../components/SimpleLoader/index.tsx";
|
||||||
|
|
||||||
const Settings = (): React.ReactNode => {
|
const Settings = (): React.ReactNode => {
|
||||||
|
const refreshAuth = useContext(RefreshAuthContext);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [dataFile, setDataFile] = useState<File>();
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// let backend handle the attachment
|
||||||
|
const exportGraph = () => {
|
||||||
|
window.open(loadExportDataFullURL);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataFileChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!e.target.files) return;
|
||||||
|
let selectedFile = e.target.files[0];
|
||||||
|
if (!selectedFile) return;
|
||||||
|
if (selectedFile.type !== "application/json") {
|
||||||
|
showWarnToastNotification("Must be JSON file!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDataFile(selectedFile);
|
||||||
|
};
|
||||||
|
|
||||||
|
const importGraph = async () => {
|
||||||
|
if (!dataFile) {
|
||||||
|
showWarnToastNotification("Select a file!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
const resp = await APIWrapper({
|
||||||
|
apiFn: apiImportGraph,
|
||||||
|
data: dataFile,
|
||||||
|
refreshAuth,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
if (resp?.status === 200) {
|
||||||
|
showSuccessToastNotification(resp.data.message);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.settings_wrapper}>
|
<div className={styles.settings_wrapper}>
|
||||||
|
{loading && <SimpleLoader />}
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
|
<hr className="divider" />
|
||||||
<div className={styles.settings_controls}>
|
<div className={styles.settings_controls}>
|
||||||
<Button>Export Data</Button>
|
<Button disabled={loading} onClickMethod={exportGraph}>
|
||||||
<Button>Import Data</Button>
|
Export Data
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
name="dataFile"
|
||||||
|
onChange={dataFileChangeHandler}
|
||||||
|
className={styles.settings_dataFile}
|
||||||
|
/>
|
||||||
|
<p className={styles.settings_dataFile_selection}>
|
||||||
|
{dataFile ? `Selected file: ${dataFile.name}` : "No file selected"}
|
||||||
|
</p>
|
||||||
|
<Button disabled={loading} onClickMethod={importGraph}>
|
||||||
|
Import Data
|
||||||
|
</Button>
|
||||||
|
<Button disabled={loading} onClickMethod={() => navigate("/logout")}>
|
||||||
|
Log Out
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user