mirror of
https://github.com/20kaushik02/spotify-manager-web.git
synced 2025-12-06 07:24:07 +00:00
ready for initial launch
styling fixes, info pages, screencaps,
This commit is contained in:
parent
e5e0751e8f
commit
c135dda6d0
BIN
public/landing-gif-2c.mp4
Normal file
BIN
public/landing-gif-2c.mp4
Normal file
Binary file not shown.
BIN
public/landing-gifc.mp4
Normal file
BIN
public/landing-gifc.mp4
Normal file
Binary file not shown.
@ -1,15 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import styles from "./AnimatedSVG.module.css";
|
import styles from "./AnimatedSVG.module.css";
|
||||||
|
|
||||||
const AnimatedSVG = (): React.ReactNode => {
|
type AnimatedSVGProps = { size?: number };
|
||||||
|
const AnimatedSVG = ({ size = 256 }: AnimatedSVGProps): React.ReactNode => {
|
||||||
const stroke = "#fff";
|
const stroke = "#fff";
|
||||||
return (
|
return (
|
||||||
<div className={styles.svg_wrapper}>
|
<div className={styles.svg_wrapper}>
|
||||||
{/* width, height and viewBox are necessary */}
|
{/* width, height and viewBox are necessary */}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="256"
|
width={size}
|
||||||
height="256" // adjust size here
|
height={size}
|
||||||
viewBox="0 0 512 512"
|
viewBox="0 0 512 512"
|
||||||
preserveAspectRatio="xMidYMid meet"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
a {
|
.base_link {
|
||||||
padding: var(--mb-3) var(--mb-1);
|
padding: var(--mb-3) var(--mb-1);
|
||||||
border-radius: 2%;
|
border-radius: 2%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--text);
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,5 +12,5 @@ a {
|
|||||||
|
|
||||||
.inactive_link {
|
.inactive_link {
|
||||||
background-color: var(--bgLinkInactive);
|
background-color: var(--bgLinkInactive);
|
||||||
box-shadow: 2px 2px var(--bg);
|
box-shadow: 6px 6px var(--bg);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,9 @@ const StyledNavLink = ({
|
|||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
to={path}
|
to={path}
|
||||||
className={({ isActive }) => (isActive ? activeClass : inactiveClass)}
|
className={({ isActive }) =>
|
||||||
|
`${styles.base_link} ${isActive ? activeClass : inactiveClass}`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|||||||
@ -69,6 +69,32 @@ ul {
|
|||||||
h1 {
|
h1 {
|
||||||
font-size: var(--headingFontSize);
|
font-size: var(--headingFontSize);
|
||||||
}
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: calc(0.9 * var(--headingFontSize));
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: calc(0.8 * var(--headingFontSize));
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: calc(0.7 * var(--headingFontSize));
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
font-size: calc(0.6 * var(--headingFontSize));
|
||||||
|
}
|
||||||
|
h6 {
|
||||||
|
font-size: calc(0.5 * var(--headingFontSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:link,
|
||||||
|
a:visited,
|
||||||
|
a:focus,
|
||||||
|
a:hover,
|
||||||
|
a:active {
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import styles from "./Graph.module.css";
|
|||||||
import { IoIosGitNetwork } from "react-icons/io";
|
import { IoIosGitNetwork } from "react-icons/io";
|
||||||
import { WiCloudRefresh } from "react-icons/wi";
|
import { WiCloudRefresh } from "react-icons/wi";
|
||||||
import { MdOutlineLock, MdOutlineLockOpen } from "react-icons/md";
|
import { MdOutlineLock, MdOutlineLockOpen } from "react-icons/md";
|
||||||
import { AiFillSpotify } from "react-icons/ai";
|
import { AiFillSpotify, AiOutlineDisconnect } from "react-icons/ai";
|
||||||
import { GiFamilyTree } from "react-icons/gi";
|
import { GiFamilyTree } from "react-icons/gi";
|
||||||
import { IoArrowDownOutline, IoArrowUpOutline } from "react-icons/io5";
|
import { IoArrowDownOutline, IoArrowUpOutline } from "react-icons/io5";
|
||||||
|
|
||||||
@ -241,6 +241,13 @@ const Graph = (): React.ReactNode => {
|
|||||||
[setLinkEdges, refreshAuth]
|
[setLinkEdges, refreshAuth]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// manually triggering edge removal
|
||||||
|
const removeSelectedEdge = async () => {
|
||||||
|
await flowInstance.deleteElements({
|
||||||
|
edges: linkEdges.filter((ed) => ed.id === selectedEdgeID),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// remove edge
|
// remove edge
|
||||||
const onFlowBeforeDelete: OnBeforeDelete = useCallback(
|
const onFlowBeforeDelete: OnBeforeDelete = useCallback(
|
||||||
async ({ nodes, edges }) => {
|
async ({ nodes, edges }) => {
|
||||||
@ -588,45 +595,63 @@ const Graph = (): React.ReactNode => {
|
|||||||
/>
|
/>
|
||||||
<Background variant={BackgroundVariant.Dots} gap={36} size={3} />
|
<Background variant={BackgroundVariant.Dots} gap={36} size={3} />
|
||||||
<Panel position="top-right">{loading && <SimpleLoader />}</Panel>
|
<Panel position="top-right">{loading && <SimpleLoader />}</Panel>
|
||||||
|
{selectedEdgeID !== "" && (
|
||||||
|
<Panel position="top-left">
|
||||||
|
<Button onClickMethod={removeSelectedEdge}>
|
||||||
|
<AiOutlineDisconnect size={36} />
|
||||||
|
Delete Link
|
||||||
|
</Button>
|
||||||
|
</Panel>
|
||||||
|
)}
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
<div className={`${styles.operations_wrapper} custom_scrollbar`}>
|
<div className={`${styles.operations_wrapper} custom_scrollbar`}>
|
||||||
<Button disabled={loading} onClickMethod={backfillLink}>
|
{linkEdges.length > 0 ? (
|
||||||
<IoArrowUpOutline size={36} />
|
<>
|
||||||
Backfill Link
|
<Button disabled={loading} onClickMethod={backfillLink}>
|
||||||
</Button>
|
<IoArrowUpOutline size={36} />
|
||||||
<Button disabled={loading} onClickMethod={backfillChain}>
|
Backfill Link
|
||||||
<span>
|
</Button>
|
||||||
<IoArrowUpOutline size={24} />
|
<Button disabled={loading} onClickMethod={backfillChain}>
|
||||||
<GiFamilyTree size={24} />
|
<span>
|
||||||
</span>
|
<IoArrowUpOutline size={24} />
|
||||||
Backfill Chain
|
<GiFamilyTree size={24} />
|
||||||
</Button>
|
</span>
|
||||||
<hr className="divider" />
|
Backfill Chain
|
||||||
<Button disabled={loading} onClickMethod={pruneLink}>
|
</Button>
|
||||||
<IoArrowDownOutline size={36} />
|
<hr className="divider" />
|
||||||
Prune Link
|
<Button disabled={loading} onClickMethod={pruneLink}>
|
||||||
</Button>
|
<IoArrowDownOutline size={36} />
|
||||||
<Button disabled={loading} onClickMethod={pruneChain}>
|
Prune Link
|
||||||
<span>
|
</Button>
|
||||||
<IoArrowDownOutline size={24} />
|
<Button disabled={loading} onClickMethod={pruneChain}>
|
||||||
<GiFamilyTree size={24} style={{ transform: "rotate(180deg)" }} />
|
<span>
|
||||||
</span>
|
<IoArrowDownOutline size={24} />
|
||||||
Prune Chain
|
<GiFamilyTree
|
||||||
</Button>
|
size={24}
|
||||||
<hr className="divider" />
|
style={{ transform: "rotate(180deg)" }}
|
||||||
<Button disabled={loading} onClickMethod={() => arrangeLayout("TB")}>
|
/>
|
||||||
<IoIosGitNetwork size={36} />
|
</span>
|
||||||
Arrange
|
Prune Chain
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled={loading} onClickMethod={toggleInteractive}>
|
<hr className="divider" />
|
||||||
{isInteractive() ? (
|
<Button
|
||||||
<MdOutlineLock size={36} />
|
disabled={loading}
|
||||||
) : (
|
onClickMethod={() => arrangeLayout("TB")}
|
||||||
<MdOutlineLockOpen size={36} />
|
>
|
||||||
)}
|
<IoIosGitNetwork size={36} />
|
||||||
{isInteractive() ? "Lock" : "Unlock"}
|
Arrange
|
||||||
</Button>
|
</Button>
|
||||||
<hr className="divider" />
|
<Button disabled={loading} onClickMethod={toggleInteractive}>
|
||||||
|
{isInteractive() ? (
|
||||||
|
<MdOutlineLock size={36} />
|
||||||
|
) : (
|
||||||
|
<MdOutlineLockOpen size={36} />
|
||||||
|
)}
|
||||||
|
{isInteractive() ? "Lock" : "Unlock"}
|
||||||
|
</Button>
|
||||||
|
<hr className="divider" />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<Button disabled={loading} onClickMethod={updateUserData}>
|
<Button disabled={loading} onClickMethod={updateUserData}>
|
||||||
<span className={styles.icons}>
|
<span className={styles.icons}>
|
||||||
<WiCloudRefresh size={36} />
|
<WiCloudRefresh size={36} />
|
||||||
|
|||||||
@ -1,6 +1,28 @@
|
|||||||
.htu_wrapper {
|
.htu_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;
|
||||||
|
gap: var(--mb-1);
|
||||||
|
padding: var(--mb-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.htu_content_wrapper {
|
||||||
|
word-wrap: break-word;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.htu_content_wrapper > ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--mb-2);
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
.htu_content_wrapper ul ul {
|
||||||
|
list-style-type: "- ";
|
||||||
|
}
|
||||||
|
.htu_content_wrapper ul ol,
|
||||||
|
.htu_content_wrapper ul ul {
|
||||||
|
margin-left: var(--mb-4);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,122 @@ const HowToUse = (): React.ReactNode => {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.htu_wrapper}>
|
<div className={styles.htu_wrapper}>
|
||||||
<h1>How To Use?</h1>
|
<h1>How To Use?</h1>
|
||||||
|
<div className={styles.htu_content_wrapper}>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<h6>Step 1: Sync your playlists</h6>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
In the{" "}
|
||||||
|
<a href="/graph">
|
||||||
|
<u>graph</u>
|
||||||
|
</a>{" "}
|
||||||
|
manager, click 'Sync Spotify' to load your playlists into the
|
||||||
|
app. This pulls your latest Spotify playlists into the
|
||||||
|
application.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
💡 Reminder: If you create or delete playlists later, you’ll
|
||||||
|
need to sync again to update your data (Might add auto-sync
|
||||||
|
later)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h6>Step 2: Build Your Graph</h6>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Click and drag from one playlist’s bottom handle to another’s
|
||||||
|
top handle to create a link
|
||||||
|
<video height={320} width={320} autoPlay loop muted playsInline>
|
||||||
|
<source
|
||||||
|
src={`${process.env["PUBLIC_URL"]}/landing-gifc.mp4`}
|
||||||
|
type="video/mp4"
|
||||||
|
/>
|
||||||
|
</video>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Click on a link and hit the Delete key or the 'Delete Link'
|
||||||
|
button to remove it.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h6>Step 3: Let The Music Flow!</h6>
|
||||||
|
<ul>
|
||||||
|
Once links exist, you can:
|
||||||
|
<li>
|
||||||
|
✅ Backfill a Link – Add missing tracks from one playlist to
|
||||||
|
another.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
✅ Backfill a Chain – Backfill iteratively across multiple
|
||||||
|
connected playlists.
|
||||||
|
</li>
|
||||||
|
<video height={320} width={480} autoPlay loop muted playsInline>
|
||||||
|
<source
|
||||||
|
src={`${process.env["PUBLIC_URL"]}/landing-gif-2c.mp4`}
|
||||||
|
type="video/mp4"
|
||||||
|
/>
|
||||||
|
</video>
|
||||||
|
<li>
|
||||||
|
✅ Prune a Link – Remove excess tracks from the source playlist.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
✅ Prune a Chain – Do the same, but across a whole sequence of
|
||||||
|
links.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h6>What is this for?</h6>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
I like to organize my own playlists as subsets/supersets. For
|
||||||
|
example: I have an 'all' playlist, that has every song I listen
|
||||||
|
to. Then I have a playlist for each genre. I have a playlist for
|
||||||
|
soundtracks of certain shows or games. Some I make for my
|
||||||
|
friends, and so on.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Then there are playlists made by others, strangers and friends
|
||||||
|
alike, that I save to my library, regularly checking for new
|
||||||
|
additions.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
This application is to:
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
make sense of the growing chaos that is a music aficionado's
|
||||||
|
Spotify library - the graph helps visualize connections, and
|
||||||
|
makes music collection/maintenance easier
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
automate the process - the REST API backend can be cURLed to
|
||||||
|
achieve this
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h6>For more:</h6>
|
||||||
|
<ul>
|
||||||
|
Check it out on GitHub:
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/20kaushik02/spotify-manager-web">
|
||||||
|
<u>The front-end - ReactJS (ReactFlow)</u>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/20kaushik02/spotify-manager">
|
||||||
|
<u>The REST API - ExpressJS</u>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,12 +1,35 @@
|
|||||||
.app_header {
|
.landing_wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 10vw;
|
margin-right: 10vw;
|
||||||
|
gap: var(--mb-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app_logo {
|
.landing_header {
|
||||||
height: 40vmin;
|
display: flex;
|
||||||
pointer-events: none;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing_content_wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--mb-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing_content {
|
||||||
|
text-align: justify;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing_links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { Link, useSearchParams } from "react-router-dom";
|
||||||
import styles from "./Landing.module.css";
|
import styles from "./Landing.module.css";
|
||||||
import {
|
import {
|
||||||
showInfoToastNotification,
|
showInfoToastNotification,
|
||||||
showSuccessToastNotification,
|
showSuccessToastNotification,
|
||||||
} from "../../components/ToastNotification/index.tsx";
|
} from "../../components/ToastNotification/index.tsx";
|
||||||
import AnimatedSVG from "../../components/AnimatedSVG/index.tsx";
|
// import AnimatedSVG from "../../components/AnimatedSVG/index.tsx";
|
||||||
|
import { FaGithub } from "react-icons/fa";
|
||||||
|
|
||||||
const Landing = (): React.ReactNode => {
|
const Landing = (): React.ReactNode => {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
@ -17,17 +18,41 @@ const Landing = (): React.ReactNode => {
|
|||||||
}
|
}
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.landing_wrapper}>
|
||||||
<header className={styles.app_header}>
|
<header className={styles.landing_header}>
|
||||||
<AnimatedSVG />
|
{/* <AnimatedSVG size={192} /> */}
|
||||||
<h1>organize your Spotify playlists as a graph</h1>
|
<div className={styles.landing_content_wrapper}>
|
||||||
|
<video height={320} width={320} autoPlay loop muted playsInline>
|
||||||
|
<source
|
||||||
|
src={`${process.env["PUBLIC_URL"]}/landing-gifc.mp4`}
|
||||||
|
type="video/mp4"
|
||||||
|
/>
|
||||||
|
</video>
|
||||||
|
<video height={320} width={480} autoPlay loop muted playsInline>
|
||||||
|
<source
|
||||||
|
src={`${process.env["PUBLIC_URL"]}/landing-gif-2c.mp4`}
|
||||||
|
type="video/mp4"
|
||||||
|
/>
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
<h2>organize your Spotify playlists as a graph</h2>
|
||||||
</header>
|
</header>
|
||||||
<ul>
|
<div className={styles.landing_content_wrapper}>
|
||||||
<li>DAG graph of your playlists</li>
|
<ul className={styles.landing_content}>
|
||||||
<li>Link them to sync tracks</li>
|
<li>📊 Visualize your playlists as a connected graph</li>
|
||||||
<li>Periodic syncing</li>
|
<li>🔗 Link playlists together to keep them in sync</li>
|
||||||
</ul>
|
<li>🔄 Fill songs from linked playlists</li>
|
||||||
</>
|
<li>✂️ Prune songs that don't belong</li>
|
||||||
|
<li>🔜 More features on the way!</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className={styles.landing_links}>
|
||||||
|
Check it out -
|
||||||
|
<Link to="https://github.com/20kaushik02/spotify-manager-web">
|
||||||
|
<FaGithub size={36} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
.settings_dataFile::before {
|
.settings_dataFile::before {
|
||||||
content: "Upload";
|
content: "Select File";
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user