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 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"
> >

View File

@ -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);
} }

View File

@ -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>

View File

@ -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%;

View File

@ -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} />

View File

@ -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);
} }

View File

@ -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, 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> </div>
); );
}; };

View File

@ -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;
} }

View File

@ -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 - &nbsp;
<Link to="https://github.com/20kaushik02/spotify-manager-web">
<FaGithub size={36} />
</Link>
</div>
</div>
); );
}; };

View File

@ -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;