ready for initial launch

styling fixes, info pages, screencaps,
This commit is contained in:
Kaushik Narayan R 2025-03-18 23:33:08 -07:00
parent e5e0751e8f
commit c135dda6d0
12 changed files with 301 additions and 64 deletions

BIN
public/landing-gif-2c.mp4 Normal file

Binary file not shown.

BIN
public/landing-gifc.mp4 Normal file

Binary file not shown.

View File

@ -1,15 +1,16 @@
import React from "react";
import styles from "./AnimatedSVG.module.css";
const AnimatedSVG = (): React.ReactNode => {
type AnimatedSVGProps = { size?: number };
const AnimatedSVG = ({ size = 256 }: AnimatedSVGProps): React.ReactNode => {
const stroke = "#fff";
return (
<div className={styles.svg_wrapper}>
{/* width, height and viewBox are necessary */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="256"
height="256" // adjust size here
width={size}
height={size}
viewBox="0 0 512 512"
preserveAspectRatio="xMidYMid meet"
>

View File

@ -1,10 +1,7 @@
a {
.base_link {
padding: var(--mb-3) var(--mb-1);
border-radius: 2%;
width: 100%;
cursor: pointer;
text-decoration: none;
color: var(--text);
text-align: center;
}
@ -15,5 +12,5 @@ a {
.inactive_link {
background-color: var(--bgLinkInactive);
box-shadow: 2px 2px var(--bg);
box-shadow: 6px 6px var(--bg);
}

View File

@ -18,7 +18,9 @@ const StyledNavLink = ({
return (
<NavLink
to={path}
className={({ isActive }) => (isActive ? activeClass : inactiveClass)}
className={({ isActive }) =>
`${styles.base_link} ${isActive ? activeClass : inactiveClass}`
}
>
{text}
</NavLink>

View File

@ -69,6 +69,32 @@ ul {
h1 {
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 {
max-width: 100%;

View File

@ -31,7 +31,7 @@ import styles from "./Graph.module.css";
import { IoIosGitNetwork } from "react-icons/io";
import { WiCloudRefresh } from "react-icons/wi";
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 { IoArrowDownOutline, IoArrowUpOutline } from "react-icons/io5";
@ -241,6 +241,13 @@ const Graph = (): React.ReactNode => {
[setLinkEdges, refreshAuth]
);
// manually triggering edge removal
const removeSelectedEdge = async () => {
await flowInstance.deleteElements({
edges: linkEdges.filter((ed) => ed.id === selectedEdgeID),
});
};
// remove edge
const onFlowBeforeDelete: OnBeforeDelete = useCallback(
async ({ nodes, edges }) => {
@ -588,45 +595,63 @@ const Graph = (): React.ReactNode => {
/>
<Background variant={BackgroundVariant.Dots} gap={36} size={3} />
<Panel position="top-right">{loading && <SimpleLoader />}</Panel>
{selectedEdgeID !== "" && (
<Panel position="top-left">
<Button onClickMethod={removeSelectedEdge}>
<AiOutlineDisconnect size={36} />
Delete Link
</Button>
</Panel>
)}
</ReactFlow>
<div className={`${styles.operations_wrapper} custom_scrollbar`}>
<Button disabled={loading} onClickMethod={backfillLink}>
<IoArrowUpOutline size={36} />
Backfill Link
</Button>
<Button disabled={loading} onClickMethod={backfillChain}>
<span>
<IoArrowUpOutline size={24} />
<GiFamilyTree size={24} />
</span>
Backfill Chain
</Button>
<hr className="divider" />
<Button disabled={loading} onClickMethod={pruneLink}>
<IoArrowDownOutline size={36} />
Prune Link
</Button>
<Button disabled={loading} onClickMethod={pruneChain}>
<span>
<IoArrowDownOutline size={24} />
<GiFamilyTree size={24} style={{ transform: "rotate(180deg)" }} />
</span>
Prune Chain
</Button>
<hr className="divider" />
<Button disabled={loading} onClickMethod={() => arrangeLayout("TB")}>
<IoIosGitNetwork size={36} />
Arrange
</Button>
<Button disabled={loading} onClickMethod={toggleInteractive}>
{isInteractive() ? (
<MdOutlineLock size={36} />
) : (
<MdOutlineLockOpen size={36} />
)}
{isInteractive() ? "Lock" : "Unlock"}
</Button>
<hr className="divider" />
{linkEdges.length > 0 ? (
<>
<Button disabled={loading} onClickMethod={backfillLink}>
<IoArrowUpOutline size={36} />
Backfill Link
</Button>
<Button disabled={loading} onClickMethod={backfillChain}>
<span>
<IoArrowUpOutline size={24} />
<GiFamilyTree size={24} />
</span>
Backfill Chain
</Button>
<hr className="divider" />
<Button disabled={loading} onClickMethod={pruneLink}>
<IoArrowDownOutline size={36} />
Prune Link
</Button>
<Button disabled={loading} onClickMethod={pruneChain}>
<span>
<IoArrowDownOutline size={24} />
<GiFamilyTree
size={24}
style={{ transform: "rotate(180deg)" }}
/>
</span>
Prune Chain
</Button>
<hr className="divider" />
<Button
disabled={loading}
onClickMethod={() => arrangeLayout("TB")}
>
<IoIosGitNetwork size={36} />
Arrange
</Button>
<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}>
<span className={styles.icons}>
<WiCloudRefresh size={36} />

View File

@ -1,6 +1,28 @@
.htu_wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
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);
}

View File

@ -4,6 +4,122 @@ const HowToUse = (): React.ReactNode => {
return (
<div className={styles.htu_wrapper}>
<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, youll
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 playlists bottom handle to anothers
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>
);
};

View File

@ -1,12 +1,35 @@
.app_header {
.landing_wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-right: 10vw;
gap: var(--mb-2);
}
.app_logo {
height: 40vmin;
pointer-events: none;
.landing_header {
display: flex;
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;
}

View File

@ -1,11 +1,12 @@
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 {
showInfoToastNotification,
showSuccessToastNotification,
} 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 [searchParams] = useSearchParams();
@ -17,17 +18,41 @@ const Landing = (): React.ReactNode => {
}
}, [searchParams]);
return (
<>
<header className={styles.app_header}>
<AnimatedSVG />
<h1>organize your Spotify playlists as a graph</h1>
<div className={styles.landing_wrapper}>
<header className={styles.landing_header}>
{/* <AnimatedSVG size={192} /> */}
<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>
<ul>
<li>DAG graph of your playlists</li>
<li>Link them to sync tracks</li>
<li>Periodic syncing</li>
</ul>
</>
<div className={styles.landing_content_wrapper}>
<ul className={styles.landing_content}>
<li>📊 Visualize your playlists as a connected graph</li>
<li>🔗 Link playlists together to keep them in sync</li>
<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 - &nbsp;
<Link to="https://github.com/20kaushik02/spotify-manager-web">
<FaGithub size={36} />
</Link>
</div>
</div>
);
};

View File

@ -24,7 +24,7 @@
visibility: hidden;
}
.settings_dataFile::before {
content: "Upload";
content: "Select File";
color: var(--text);
display: flex;
flex-direction: column;