This commit is contained in:
Kaushik Narayan R 2024-12-29 00:46:08 -07:00
parent 562ba51992
commit 41c3f26829
22 changed files with 353 additions and 76 deletions

25
.editorconfig Normal file
View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
# top-most EditorConfig file
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
max_line_length = 80
[*.txt]
indent_style = tab
indent_size = 4
[*.{diff,md}]
trim_trailing_whitespace = false

View File

@ -1,9 +0,0 @@
.App {
min-height: 100vh;
display: flex;
text-align: center;
align-items: center;
justify-content: center;
background-color: #282c34;
color: white;
}

View File

@ -4,7 +4,7 @@ import { BrowserRouter } from "react-router-dom";
import { ToastContainer } from "react-toastify"; import { ToastContainer } from "react-toastify";
// Styles // Styles
import './App.css'; import styles from './App.module.css';
// Assets // Assets
@ -94,11 +94,13 @@ function App() {
return ( return (
<WidthContext.Provider value={width}> <WidthContext.Provider value={width}>
<AuthContext.Provider value={auth}> <AuthContext.Provider value={auth}>
<div className="App"> <div className={styles.app_wrapper}>
<BrowserRouter> <BrowserRouter>
<ScrollToTop /> <ScrollToTop />
<Navbar /> <Navbar />
<div className={styles.page_wrapper}>
<AllRoutes /> <AllRoutes />
</div>
</BrowserRouter> </BrowserRouter>
<ToastContainer <ToastContainer
id={"notif-container"} id={"notif-container"}

16
src/App.module.css Normal file
View File

@ -0,0 +1,16 @@
.app_wrapper {
min-height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: space-evenly;
}
.page_wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: calc(100%);
margin-right: 10vw;
}

View File

@ -1,7 +1,5 @@
import { axiosInstance } from "./axiosInstance"; import { axiosInstance } from "./axiosInstance";
import { authHealthCheckURL, authRefreshURL } from "./paths";
const authHealthCheckURL = "auth-health";
const authRefreshURL = "api/auth/refresh";
export const apiAuthCheck = async () => { export const apiAuthCheck = async () => {
try { try {

View File

@ -1,7 +1,8 @@
import axios from "axios"; import axios from "axios";
import { backendDomain } from "./paths";
export const axiosInstance = axios.create({ export const axiosInstance = axios.create({
baseURL: process.env.REACT_APP_API_BASE_URL, baseURL: backendDomain,
withCredentials: true, withCredentials: true,
timeout: 20000, timeout: 20000,
headers: { headers: {

7
src/api/paths.js Normal file
View File

@ -0,0 +1,7 @@
export const backendDomain = process.env.REACT_APP_API_BASE_URL + "/";
export const authLoginURL = backendDomain + "api/auth/login"
export const authLogoutURL = backendDomain + "api/auth/logout"
export const authHealthCheckURL = "auth-health";
export const authRefreshURL = "api/auth/refresh";

View File

@ -1,13 +1,14 @@
<?xml version="1.0" standalone="no"?> <?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" <svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000" width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet"> preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)" <g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)">
fill="#000000" stroke="none"> <path
<path d="M1480 4540 c-20 -20 -20 -33 -20 -611 0 -379 4 -597 10 -609 19 -36 id="svglength"
fill="none" stroke="#fff" stroke-width="32px"
d="M1480 4540 c-20 -20 -20 -33 -20 -611 0 -379 4 -597 10 -609 19 -36
72 -40 548 -40 l462 0 0 -275 0 -275 -881 0 c-650 0 -886 -3 -902 -12 -46 -24 72 -40 548 -40 l462 0 0 -275 0 -275 -881 0 c-650 0 -886 -3 -902 -12 -46 -24
-46 -26 -47 -375 l0 -333 -289 0 c-159 0 -301 -4 -314 -9 -47 -18 -48 -23 -45 -46 -26 -47 -375 l0 -333 -289 0 c-159 0 -301 -4 -314 -9 -47 -18 -48 -23 -45
-734 l3 -669 24 -19 c22 -18 49 -19 696 -19 665 0 673 0 699 21 l26 20 0 683 -734 l3 -669 24 -19 c22 -18 49 -19 696 -19 665 0 673 0 699 21 l26 20 0 683
@ -23,6 +24,6 @@ c-22 23 -26 23 -322 26 l-300 3 0 279 0 280 835 0 835 0 0 -280 0 -279 -300
l0 -475 -940 0 -940 0 0 475 0 475 940 0 940 0 0 -475z m-2202 -2642 l2 -563 l0 -475 -940 0 -940 0 0 475 0 475 940 0 940 0 0 -475z m-2202 -2642 l2 -563
-570 0 -570 0 0 565 0 565 568 -2 567 -3 3 -562z m1832 2 l0 -565 -570 0 -570 -570 0 -570 0 0 565 0 565 568 -2 567 -3 3 -562z m1832 2 l0 -565 -570 0 -570
0 0 565 0 565 570 0 570 0 0 -565z m1830 0 l0 -565 -570 0 -570 0 0 558 c0 0 0 565 0 565 570 0 570 0 0 -565z m1830 0 l0 -565 -570 0 -570 0 0 558 c0
307 3 562 7 565 3 4 260 7 570 7 l563 0 0 -565z"/> 307 3 562 7 565 3 4 260 7 570 7 l563 0 0 -565z" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,20 @@
/* the value 54150 is decided by getting the length of the path (54122 for the logo asset) */
.svgWrapper path {
stroke-dasharray: 54150;
stroke-dashoffset: 54150;
animation: draw 5s ease-in-out infinite;
animation-fill-mode: both;
}
@keyframes draw {
0% {
stroke-dashoffset: 54150;
}
60% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: 54150;
}
}

View File

@ -0,0 +1,45 @@
import React from 'react';
import styles from "./AnimatedSVG.module.css";
const AnimatedSVG = ({ stroke = "#ffffff" }) => {
return (
<div className={styles.svgWrapper}>
{/* width, height and viewBox are necessary */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="512" height="512" viewBox="0 0 512 512"
preserveAspectRatio="xMidYMid meet"
>
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)">
<path
d="M1480 4540 c-20 -20 -20 -33 -20 -611 0 -379 4 -597 10 -609 19 -36
72 -40 548 -40 l462 0 0 -275 0 -275 -881 0 c-650 0 -886 -3 -902 -12 -46 -24
-46 -26 -47 -375 l0 -333 -289 0 c-159 0 -301 -4 -314 -9 -47 -18 -48 -23 -45
-734 l3 -669 24 -19 c22 -18 49 -19 696 -19 665 0 673 0 699 21 l26 20 0 683
c0 681 0 683 -21 702 -20 18 -42 19 -320 22 l-299 3 0 279 0 280 835 0 835 0
0 -280 0 -279 -300 -3 c-296 -3 -300 -3 -322 -26 l-23 -23 0 -680 0 -681 24
-19 c22 -18 49 -19 701 -19 652 0 679 1 701 19 l24 19 0 681 0 680 -23 23
c-22 23 -26 23 -322 26 l-300 3 0 279 0 280 835 0 835 0 0 -280 0 -279 -300
-3 c-296 -3 -300 -3 -322 -26 l-23 -23 -3 -647 c-2 -356 0 -662 3 -680 3 -18
15 -42 26 -53 18 -18 39 -19 698 -19 653 0 680 1 702 19 l24 19 0 681 0 680
-23 23 c-22 23 -26 23 -322 26 l-300 3 0 329 c0 180 -3 336 -6 345 -18 45 -9
45 -944 45 l-880 0 0 275 0 275 463 0 c475 0 528 4 547 40 6 12 10 230 10 607
0 579 0 590 -20 611 l-21 22 -1059 0 c-1047 0 -1060 0 -1080 -20z m2020 -615
l0 -475 -940 0 -940 0 0 475 0 475 940 0 940 0 0 -475z m-2202 -2642 l2 -563
-570 0 -570 0 0 565 0 565 568 -2 567 -3 3 -562z m1832 2 l0 -565 -570 0 -570
0 0 565 0 565 570 0 570 0 0 -565z m1830 0 l0 -565 -570 0 -570 0 0 558 c0
307 3 562 7 565 3 4 260 7 570 7 l563 0 0 -565z"
stroke={stroke}
strokeWidth="32px"
strokeMiterlimit={10}
fill="none"
id="svglength"
// document.getElementById('svglength').getTotalLength()
/>
</g>
</svg>
</div>
)
}
export default AnimatedSVG;

View File

@ -2,7 +2,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: space-evenly; justify-content: space-around;
position: sticky; position: sticky;
top: 0; top: 0;
left: 0; left: 0;

View File

@ -1,14 +1,23 @@
import React from 'react' import React, { useContext } from 'react'
import styles from "./Navbar.module.css"; import styles from "./Navbar.module.css";
import { NavLink } from 'react-router-dom';
import { AuthContext } from "../../App";
import StyledNavLink from '../StyledNavLink';
const Navbar = () => { const Navbar = () => {
const auth = useContext(AuthContext);
return ( return (
<nav className={styles.navbar_wrapper}> <nav className={styles.navbar_wrapper}>
<NavLink to="/">Home</NavLink> <StyledNavLink path="/" text="Home" />
<NavLink to="/login">Login</NavLink> {
<NavLink to="/graph">Graph</NavLink> auth === true ?
<NavLink to="/graph2">Graph2</NavLink> <StyledNavLink path="/logout" text="Logout" /> :
<StyledNavLink path="/login" text="Login" />
}
<StyledNavLink path="/graph" text="Graph" />
<StyledNavLink path="/about" text="About" />
</nav> </nav>
) )
} }

View File

@ -0,0 +1,12 @@
.link_wrapper {
padding: 2em;
border-radius: 20%;
}
.active_link {
}
.inactive_link {
}

View File

@ -0,0 +1,33 @@
import React from 'react'
import { NavLink } from 'react-router-dom';
import styles from "./StyledNavLink.module.css";
/**
* @param {{
* path: string,
* text: string,
* activeClass: string,
* inactiveClass: string
* }}
* @returns
*/
const StyledNavLink = ({
path = "/",
text = "Go To",
activeClass = styles.active_link,
inactiveClass = styles.inactive_link
}) => {
return (
<div className={styles.link_wrapper}>
<NavLink
to={path}
className={({ isActive }) => isActive ? activeClass : inactiveClass}
>
{text}
</NavLink>
</div>
)
}
export default StyledNavLink;

View File

@ -1,13 +1,110 @@
body { /*===== CSS GLOBAL VARIABLES =====*/
/* Font Size */
:root {
--normalFontSize: 16px;
--headingFont: AileronFont;
--primaryFont: AileronFont;
--text: whitesmoke;
--bgBlue: #040a53;
--darkBlue: #061320;
--vscBlue: #1b81a8;
--dYellow: #5b3c15;
--lYellow: #fcbe72;
--primary: #04005e;
--secondry: #440bd4;
--tertiary: #ff2079;
--accent: #e92efb;
--bg: #262626;
--blue1: #405de6;
--blue2: #5851d8;
--purple1: #833ab4;
--pink1: #c13584;
--pink2: #e1306c;
--red1: #fd1d1d;
--orange1: #f56040;
--orange2: #f77737;
--orange3: #fcaf45;
--orange4: #ffdc80;
--fun_b: #b00b69;
--fun_d: #1d1cc5;
}
/* Colors */
/* Fonts */
@font-face {
font-family: AileronFont;
src: url(/src/assets/fonts/Aileron-Black.otf);
font-weight: bold;
}
@font-face {
font-family: AileronFont;
src: url(/src/assets/fonts/Aileron-Light.otf);
font-weight: light;
}
/* rem-based (root element) sizes */
:root {
--mb-1: 0.5rem;
--mb-2: 1rem;
--mb-3: 1.5rem;
--mb-4: 2rem;
--mb-5: 2.5rem;
--mb-6: 3rem;
}
/*===== z-index =====*/
:root {
--z-back: -10;
--z-normal: 1;
--z-tooltip: 10;
--z-fixed: 100;
}
/*===== BASE =====*/
*,
::before,
::after {
box-sizing: border-box;
}
* {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', padding: 0;
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', }
html {
scroll-behavior: smooth;
}
ul {
padding: 0;
list-style: outside;
}
img {
max-width: 100%;
height: auto;
display: block;
}
body {
font-family: var(--primaryFont),-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif; sans-serif;
font-size: var(--normalFontSize);
color: var(--text);
background-color: var(--bgBlue);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
letter-spacing: 1.5px;
} }
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace; monospace;
} }

View File

@ -1,6 +1,5 @@
.graph_wrapper { .graph_wrapper {
display: flex; display: flex;
position: inherit;
width: 100%; width: 100%;
height: 80vh; height: 100vh;
} }

View File

@ -10,18 +10,3 @@
height: 40vmin; height: 40vmin;
pointer-events: none; pointer-events: none;
} }
@media (prefers-reduced-motion: no-preference) {
.app_logo {
animation: App-logo-spin infinite 20s linear;
}
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -2,7 +2,8 @@ import React, { useEffect } from "react"
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import styles from "./Landing.module.css" import styles from "./Landing.module.css"
import logo from '../../assets/icons/logo.svg'; import logo from '../../assets/icons/logo.svg';
import { showSuccessToastNotification } from "../../components/ToastNotification"; import { showInfoToastNotification, showSuccessToastNotification } from "../../components/ToastNotification";
import AnimatedSVG from "../../components/AnimatedSVG";
const Landing = () => { const Landing = () => {
@ -11,17 +12,22 @@ const Landing = () => {
useEffect(() => { useEffect(() => {
if (searchParams.get("login") === "success") { if (searchParams.get("login") === "success") {
showSuccessToastNotification("Logged in!"); showSuccessToastNotification("Logged in!");
} else if (searchParams.get("logout") === "success") {
showInfoToastNotification("Logged out.");
} }
}, [searchParams]); }, [searchParams]);
return ( return (
<>
<header className={styles.app_header}> <header className={styles.app_header}>
<img src={logo} className={styles.app_logo} alt="logo" /> <AnimatedSVG />
<h1>Organize your Spotify playlists as a graph.</h1> <h1>organize your Spotify playlists as a graph</h1>
<h5>Features:</h5>
<ul>
<li>blah 1</li>
</ul>
</header> </header>
<ul>
<li>DAG graph of your playlists</li>
<li>Link them to sync tracks</li>
<li>Periodic syncing</li>
</ul>
</>
) )
} }

View File

@ -1,17 +1,20 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import styles from './Login.module.css'; import styles from './Login.module.css';
import { authLoginURL } from '../../api/paths';
// auth through backend // auth through backend
const Login = () => { const Login = () => {
useEffect(() => { useEffect(() => {
const timeoutID = setTimeout(() => { const timeoutID = setTimeout(() => {
window.open(process.env.REACT_APP_API_BASE_URL + "/api/auth/login", "_self") window.open(authLoginURL, "_self")
}, 1000); }, 1000);
return () => clearTimeout(timeoutID); return () => clearTimeout(timeoutID);
}, []); }, []);
return ( return (
<div className={styles.login_wrapper}>Redirecting to Spotify...</div> <div className={styles.login_wrapper}>
Redirecting to Spotify...
</div>
) )
} }

View File

@ -0,0 +1,6 @@
.logout_wrapper {
display: flex;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
}

View File

@ -0,0 +1,20 @@
import React, { useEffect } from 'react';
import styles from './Logout.module.css';
import { authLogoutURL } from '../../api/paths';
const Logout = () => {
useEffect(() => {
const timeoutID = setTimeout(() => {
window.open(authLogoutURL, "_self")
}, 1000);
return () => clearTimeout(timeoutID);
}, []);
return (
<div className={styles.logout_wrapper}>
See you soon!
</div>
)
}
export default Logout;

View File

@ -7,12 +7,14 @@ import Landing from "../pages/Landing";
import PageNotFound from "../pages/PageNotFound"; import PageNotFound from "../pages/PageNotFound";
import Graph from "../pages/Graph"; import Graph from "../pages/Graph";
import Login from "../pages/Login"; import Login from "../pages/Login";
import Logout from "../pages/Logout";
const AllRoutes = () => { const AllRoutes = () => {
return ( return (
<Routes> <Routes>
{/* Routes that require user to be logged in */} {/* Routes that require user to be logged in */}
<Route element={<AuthOnlyRoutes />}> <Route element={<AuthOnlyRoutes />}>
<Route path="/logout" element={<Logout />} />
<Route path="/graph" element={<Graph />} /> <Route path="/graph" element={<Graph />} />
{/* <Route path="/playlists" element={<Playlists />} /> */} {/* <Route path="/playlists" element={<Playlists />} /> */}
</Route> </Route>
@ -25,7 +27,6 @@ const AllRoutes = () => {
{/* Common routes */} {/* Common routes */}
<Route path="/" element={<Landing />} /> <Route path="/" element={<Landing />} />
<Route path="/graph2" element={<Graph />} />
{/* 404 */} {/* 404 */}
<Route path="/page-not-found" element={<PageNotFound />} /> <Route path="/page-not-found" element={<PageNotFound />} />
<Route path="*" element={<PageNotFound />} /> <Route path="*" element={<PageNotFound />} />