mirror of
https://github.com/20kaushik02/spotify-manager-web.git
synced 2025-12-06 06:14: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"
|
||||
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="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<meta name="description" content="Frontend for spotify-manager" />
|
||||
<!--
|
||||
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/
|
||||
@ -40,7 +36,7 @@
|
||||
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`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>Spotify Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"short_name": "Spotify Manager",
|
||||
"name": "Spotify Manager",
|
||||
"icons": [
|
||||
{
|
||||
"src": "android-chrome-192x192.png",
|
||||
|
||||
@ -33,7 +33,7 @@ export const RefreshAuthContext: Context<() => Promise<boolean>> =
|
||||
function App(): React.ReactNode {
|
||||
// States
|
||||
const [width, setWidth] = useState(0);
|
||||
const [auth, setAuth] = useState(false);
|
||||
const [auth, setAuth] = useState(true);
|
||||
|
||||
const refreshAuth = async () => {
|
||||
// reauth
|
||||
@ -82,7 +82,9 @@ function App(): React.ReactNode {
|
||||
showErrorToastNotification(resp.data.message);
|
||||
return false;
|
||||
}
|
||||
if (resp.status === 401) await refreshAuth();
|
||||
if (resp.status === 401) {
|
||||
return await refreshAuth();
|
||||
}
|
||||
setAuth(false);
|
||||
showWarnToastNotification(resp.data.message);
|
||||
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 opPruneLinkURL = "api/operations/prune/link";
|
||||
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 (
|
||||
<nav className={`${styles.navbar_wrapper} custom_scrollbar`}>
|
||||
<StyledNavLink path="/" text="About" />
|
||||
<StyledNavLink path="/" text="Home" />
|
||||
<StyledNavLink path="/graph" text="Graph" />
|
||||
<StyledNavLink path="/how-to" text="How To" />
|
||||
<StyledNavLink path="/settings" text="Settings" />
|
||||
{auth === true ? (
|
||||
<StyledNavLink path="/logout" text="Logout" />
|
||||
<StyledNavLink path="/settings" text="Settings" />
|
||||
) : (
|
||||
<StyledNavLink path="/login" text="Login" />
|
||||
)}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
/* Font Size */
|
||||
:root {
|
||||
--normalFontSize: 16px;
|
||||
--headingFontSize: 24px;
|
||||
--headingFontSize: 48px;
|
||||
--headingFont: AileronFont;
|
||||
--primaryFont: AileronFont;
|
||||
--text: whitesmoke;
|
||||
@ -66,6 +66,10 @@ ul {
|
||||
list-style: outside;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--headingFontSize);
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
@ -106,3 +110,11 @@ code {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
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;
|
||||
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!");
|
||||
return false;
|
||||
}
|
||||
if (!edges[0]) throw new ReferenceError("no edge selected");
|
||||
if (!edges[0]) {
|
||||
showWarnToastNotification("Select a link!");
|
||||
return false;
|
||||
}
|
||||
console.debug(
|
||||
`deleted connection: ${edges[0].source} -> ${edges[0].target}`
|
||||
);
|
||||
@ -598,7 +601,7 @@ const Graph = (): React.ReactNode => {
|
||||
</span>
|
||||
Backfill Chain
|
||||
</Button>
|
||||
<hr className={styles.divider} />
|
||||
<hr className="divider" />
|
||||
<Button disabled={loading} onClickMethod={pruneLink}>
|
||||
<IoArrowDownOutline size={36} />
|
||||
Prune Link
|
||||
@ -610,7 +613,7 @@ const Graph = (): React.ReactNode => {
|
||||
</span>
|
||||
Prune Chain
|
||||
</Button>
|
||||
<hr className={styles.divider} />
|
||||
<hr className="divider" />
|
||||
<Button disabled={loading} onClickMethod={() => arrangeLayout("TB")}>
|
||||
<IoIosGitNetwork size={36} />
|
||||
Arrange
|
||||
@ -623,7 +626,7 @@ const Graph = (): React.ReactNode => {
|
||||
)}
|
||||
{isInteractive() ? "Lock" : "Unlock"}
|
||||
</Button>
|
||||
<hr className={styles.divider} />
|
||||
<hr className="divider" />
|
||||
<Button disabled={loading} onClickMethod={updateUserData}>
|
||||
<span className={styles.icons}>
|
||||
<WiCloudRefresh size={36} />
|
||||
|
||||
@ -3,5 +3,4 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10vw;
|
||||
font-size: var(--headingFontSize);
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10vw;
|
||||
font-size: var(--headingFontSize);
|
||||
}
|
||||
|
||||
.app_logo {
|
||||
|
||||
@ -3,5 +3,4 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10vw;
|
||||
font-size: var(--headingFontSize);
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import styles from "./Login.module.css";
|
||||
import { authLoginFullURL } from "../../api/paths.ts";
|
||||
|
||||
// auth through backend
|
||||
const Login = ():React.ReactNode => {
|
||||
const Login = (): React.ReactNode => {
|
||||
useEffect(() => {
|
||||
const timeoutID = setTimeout(() => {
|
||||
window.open(authLoginFullURL, "_self");
|
||||
@ -11,7 +11,11 @@ const Login = ():React.ReactNode => {
|
||||
|
||||
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;
|
||||
|
||||
@ -3,5 +3,4 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10vw;
|
||||
font-size: var(--headingFontSize);
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { useEffect } from "react";
|
||||
import styles from "./Logout.module.css";
|
||||
import { authLogoutFullURL } from "../../api/paths.ts";
|
||||
|
||||
const Logout = ():React.ReactNode => {
|
||||
const Logout = (): React.ReactNode => {
|
||||
useEffect(() => {
|
||||
const timeoutID = setTimeout(() => {
|
||||
window.open(authLogoutFullURL, "_self");
|
||||
@ -10,7 +10,11 @@ const Logout = ():React.ReactNode => {
|
||||
|
||||
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;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
.pnf_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10vw;
|
||||
font-size: var(--headingFontSize);
|
||||
}
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
import React, { useEffect } from "react";
|
||||
import styles from "./PageNotFound.module.css";
|
||||
import { showWarnToastNotification } from "../../components/ToastNotification/index.tsx";
|
||||
import AnimatedSVG from "../../components/AnimatedSVG/index.tsx";
|
||||
|
||||
const PageNotFound = ():React.ReactNode => {
|
||||
const PageNotFound = (): React.ReactNode => {
|
||||
useEffect(() => {
|
||||
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;
|
||||
|
||||
@ -4,10 +4,45 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10vw;
|
||||
font-size: var(--headingFontSize);
|
||||
}
|
||||
|
||||
.settings_controls {
|
||||
padding: var(--mb-4);
|
||||
width: 20vw;
|
||||
gap: var(--mb-2);
|
||||
padding: var(--mb-2);
|
||||
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 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 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 (
|
||||
<div className={styles.settings_wrapper}>
|
||||
{loading && <SimpleLoader />}
|
||||
<h1>Settings</h1>
|
||||
<hr className="divider" />
|
||||
<div className={styles.settings_controls}>
|
||||
<Button>Export Data</Button>
|
||||
<Button>Import Data</Button>
|
||||
<Button disabled={loading} onClickMethod={exportGraph}>
|
||||
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>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user