Changes...

This commit is contained in:
william 2023-01-24 22:37:57 -05:00
parent f4fcae38e1
commit 9dacd1944d
28 changed files with 652 additions and 95 deletions

12
.fleet/run.json Normal file
View File

@ -0,0 +1,12 @@
{
"configurations": [
{
"name": "Start Frontend",
"type": "command",
"program": "npm",
"args": [
"start"
]
}
]
}

0
.fleet/settings.json Normal file
View File

115
package-lock.json generated
View File

@ -15,14 +15,17 @@
"@types/node": "^16.18.10", "@types/node": "^16.18.10",
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9", "@types/react-dom": "^18.0.9",
"@types/react-router-bootstrap": "^0.24.5",
"axios": "^1.2.1", "axios": "^1.2.1",
"bootstrap": "^5.2.3", "bootstrap": "^5.2.3",
"bootstrap-icons": "^1.10.3",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.7.0", "react-bootstrap": "^2.7.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-bootstrap": "^0.26.2", "react-router-bootstrap": "^0.26.2",
"react-router-dom": "^6.6.0", "react-router-dom": "^6.6.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"sass": "^1.57.1",
"typescript": "^4.9.4", "typescript": "^4.9.4",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
} }
@ -3817,6 +3820,11 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/history": {
"version": "4.7.11",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA=="
},
"node_modules/@types/html-minifier-terser": { "node_modules/@types/html-minifier-terser": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@ -3928,6 +3936,34 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"node_modules/@types/react-router": {
"version": "5.1.20",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
"integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
"dependencies": {
"@types/history": "^4.7.11",
"@types/react": "*"
}
},
"node_modules/@types/react-router-bootstrap": {
"version": "0.24.5",
"resolved": "https://registry.npmjs.org/@types/react-router-bootstrap/-/react-router-bootstrap-0.24.5.tgz",
"integrity": "sha512-GRx/8xF/skw4/Pmm6d+xbExi8gobCLOe8Eoz9kXPQGbYo7p5Wbi61tjpOF5AbfJ5XMN+fIzweToTi56odj/LOQ==",
"dependencies": {
"@types/react": "*",
"@types/react-router-dom": "*"
}
},
"node_modules/@types/react-router-dom": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
"integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
"dependencies": {
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router": "*"
}
},
"node_modules/@types/react-transition-group": { "node_modules/@types/react-transition-group": {
"version": "4.4.5", "version": "4.4.5",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
@ -5266,6 +5302,11 @@
"@popperjs/core": "^2.11.6" "@popperjs/core": "^2.11.6"
} }
}, },
"node_modules/bootstrap-icons": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.3.tgz",
"integrity": "sha512-7Qvj0j0idEm/DdX9Q0CpxAnJYqBCFCiUI6qzSPYfERMcokVuV9Mdm/AJiVZI8+Gawe4h/l6zFcOzvV7oXCZArw=="
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -8748,6 +8789,11 @@
"url": "https://opencollective.com/immer" "url": "https://opencollective.com/immer"
} }
}, },
"node_modules/immutable": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.1.tgz",
"integrity": "sha512-7WYV7Q5BTs0nlQm7tl92rDYYoyELLKHoDMBKhrxEoiV4mrfVdRz8hzPiYOzH7yWjzoVEamxRuAqhxL2PLRwZYQ=="
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -14742,6 +14788,22 @@
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz",
"integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA=="
}, },
"node_modules/sass": {
"version": "1.57.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz",
"integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/sass-loader": { "node_modules/sass-loader": {
"version": "12.6.0", "version": "12.6.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
@ -19642,6 +19704,11 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/history": {
"version": "4.7.11",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA=="
},
"@types/html-minifier-terser": { "@types/html-minifier-terser": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@ -19753,6 +19820,34 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"@types/react-router": {
"version": "5.1.20",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
"integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
"requires": {
"@types/history": "^4.7.11",
"@types/react": "*"
}
},
"@types/react-router-bootstrap": {
"version": "0.24.5",
"resolved": "https://registry.npmjs.org/@types/react-router-bootstrap/-/react-router-bootstrap-0.24.5.tgz",
"integrity": "sha512-GRx/8xF/skw4/Pmm6d+xbExi8gobCLOe8Eoz9kXPQGbYo7p5Wbi61tjpOF5AbfJ5XMN+fIzweToTi56odj/LOQ==",
"requires": {
"@types/react": "*",
"@types/react-router-dom": "*"
}
},
"@types/react-router-dom": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
"integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
"requires": {
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router": "*"
}
},
"@types/react-transition-group": { "@types/react-transition-group": {
"version": "4.4.5", "version": "4.4.5",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
@ -20759,6 +20854,11 @@
"integrity": "sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==", "integrity": "sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==",
"requires": {} "requires": {}
}, },
"bootstrap-icons": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.3.tgz",
"integrity": "sha512-7Qvj0j0idEm/DdX9Q0CpxAnJYqBCFCiUI6qzSPYfERMcokVuV9Mdm/AJiVZI8+Gawe4h/l6zFcOzvV7oXCZArw=="
},
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -23291,6 +23391,11 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.16.tgz", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.16.tgz",
"integrity": "sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ==" "integrity": "sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ=="
}, },
"immutable": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.1.tgz",
"integrity": "sha512-7WYV7Q5BTs0nlQm7tl92rDYYoyELLKHoDMBKhrxEoiV4mrfVdRz8hzPiYOzH7yWjzoVEamxRuAqhxL2PLRwZYQ=="
},
"import-fresh": { "import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -27417,6 +27522,16 @@
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz",
"integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA=="
}, },
"sass": {
"version": "1.57.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz",
"integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==",
"requires": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
}
},
"sass-loader": { "sass-loader": {
"version": "12.6.0", "version": "12.6.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",

View File

@ -10,14 +10,17 @@
"@types/node": "^16.18.10", "@types/node": "^16.18.10",
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9", "@types/react-dom": "^18.0.9",
"@types/react-router-bootstrap": "^0.24.5",
"axios": "^1.2.1", "axios": "^1.2.1",
"bootstrap": "^5.2.3", "bootstrap": "^5.2.3",
"bootstrap-icons": "^1.10.3",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.7.0", "react-bootstrap": "^2.7.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-bootstrap": "^0.26.2", "react-router-bootstrap": "^0.26.2",
"react-router-dom": "^6.6.0", "react-router-dom": "^6.6.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"sass": "^1.57.1",
"typescript": "^4.9.4", "typescript": "^4.9.4",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },

View File

@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -1,21 +1,20 @@
import axios from 'axios';
import React, {useEffect, useState} from 'react'; import React, {useEffect, useState} from 'react';
import './App.css'; import TrackDisplay from './components/TrackDisplay';
import SongDisplay from './components/SongDisplay'; import api from "./api";
import Song from './model/song.model'; import Track from './model/track.model';
const App: React.FC = () => { const App: React.FC = () => {
const [songs, setSongs] = useState(new Array<Song>()); const [tracks, setTracks] = useState(new Array<Track>());
useEffect(() => { useEffect(() => {
axios.get("http://localhost:8080/api/v1/song/") api.get("/tracks/")
.then(response => setSongs(response.data)); .then(response => setTracks(response.data));
}, []); }, []);
return ( return (
<div className="App"> <div className="App">
{songs.map(s => ( {tracks.map(t => (
<SongDisplay key={s.id} song={s}/> <TrackDisplay key={t.id} track={t}/>
))} ))}
</div> </div>
); );

8
src/api.ts Normal file
View File

@ -0,0 +1,8 @@
import axios from "axios";
const api = axios.create({
baseURL: "http://localhost:8080/api/v1",
withCredentials: true
});
export default api;

View File

@ -0,0 +1,43 @@
import React, {FC} from "react";
import ExternalTrack from "../model/externalTrack.model";
import Icon from "./Icon";
const ExternalTrackCard: FC<ExternalTrackCardProps> = ({
track,
source,
imported,
previewing,
onImportClick,
onPreviewClick,
onSpotifyIconClick
}) => (
<div className="external-track-card">
<img className="external-track-thumbnail" src={track.thumbnailUrl} alt={'Thumbnail - ' + track.name}/>
<div className="external-track-description">
<p className="external-track-title" onClick={onSpotifyIconClick}
title={'Open on ' + source}>{track.name}</p>
<p className="external-track-subtitle">{track.authors.join(', ') + ' - ' + track.albumName}</p>
</div>
<div className="external-track-actions">
{previewing ?
<Icon name="pause" hover="pause-fill" onClick={onPreviewClick} title="Stop preview"/> :
<Icon name="play" hover="play-fill" onClick={onPreviewClick} title="Preview track"/>}
{imported ?
<Icon name="check" hover="check-circle-fill" onClick={onImportClick} title="Remove from library"/> :
<Icon name="plus" hover="plus-circle-fill" onClick={onImportClick} title="Import in library"/>}
</div>
</div>
);
interface ExternalTrackCardProps {
track: ExternalTrack,
source: string,
imported: boolean,
previewing: boolean,
onImportClick: () => void,
onPreviewClick: () => void,
onSpotifyIconClick: () => void
}
export default ExternalTrackCard;

35
src/components/Icon.tsx Normal file
View File

@ -0,0 +1,35 @@
import {FC, useEffect, useState} from "react";
const Icon: FC<IconProps> = ({name, hover, title, onClick}) => {
const [hovering, setHovering] = useState(false);
const [className, setClassName] = useState("bi bi-" + name);
useEffect(() => {
let icon;
if (hover && hovering) {
icon = hover;
} else {
icon = name;
}
setClassName("bi bi-" + icon);
}, [hovering, name, hover]);
return (
<i className={className}
title={title}
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
onClick={onClick}></i>
);
};
interface IconProps {
name: string
hover?: string
title?: string
onClick?: () => void
}
export default Icon;

27
src/components/NavTab.tsx Normal file
View File

@ -0,0 +1,27 @@
import React, {FC} from "react";
import {Nav} from "react-bootstrap";
import {Link} from "react-router-dom";
const NavTab: FC<NavTabProps> = ({eventKey, label, icon, activeIcon, activeKey, onTabClick}) => {
const isActive = () => activeKey === eventKey;
return (
<Nav.Link as={Link} eventKey={eventKey} to={'/' + eventKey} onClick={() => onTabClick(eventKey)}>
{!isActive() || activeIcon == null ?
<i className={'bi bi-' + icon}></i> :
<i className={'bi bi-' + activeIcon}></i>}
<span>{label}</span>
</Nav.Link>
);
};
interface NavTabProps {
eventKey: string,
label: string,
icon: string,
activeIcon?: string,
activeKey: string,
onTabClick: (key: string) => void
}
export default NavTab;

View File

@ -0,0 +1,29 @@
import {FC, FormEvent, useState} from "react";
const SearchSongForm: FC<SearchSongFormProps> = ({submitSearchHandler}) => {
const [searchQuery, setSearchQuery] = useState("");
const submitFormHandler = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
submitSearchHandler(searchQuery);
};
return (
<form onSubmit={submitFormHandler}>
<input
name="query"
type="string"
placeholder="Search query"
required
value={searchQuery}
onChange={e => setSearchQuery(e.currentTarget.value)}/>
<button>Search</button>
</form>
);
};
interface SearchSongFormProps {
submitSearchHandler: (query: string) => void
}
export default SearchSongForm;

View File

@ -1,16 +0,0 @@
import React from "react";
import Song from "../model/song.model";
const SongDisplay: React.FC<SongDisplayProps> = ({song}) => {
return (
<div>
<p>{song.name} - {song.authors.join(" and ")}</p>
</div>
);
};
export interface SongDisplayProps {
song: Song
}
export default SongDisplay;

View File

@ -0,0 +1,16 @@
import React from "react";
import Track from "../model/track.model";
const TrackDisplay: React.FC<TrackDisplayProps> = ({track}) => {
return (
<div>
<p>{track.name} - {track.authors.join(" and ")}</p>
</div>
);
};
export interface TrackDisplayProps {
track: Track
}
export default TrackDisplay;

View File

@ -0,0 +1,28 @@
import {FC, useState} from "react";
import {Container, Nav, Navbar} from "react-bootstrap";
import NavTab from "../components/NavTab";
import "../style/navbar.scss";
const NavbarContainer: FC = () => {
const [activeKey, setActiveKey] = useState("library");
return (
<Navbar bg="dark">
<Container>
<Nav className="me-auto" activeKey={activeKey}>
<NavTab eventKey="library" label="Library" icon="vinyl" activeIcon="vinyl-fill"
activeKey={activeKey} onTabClick={setActiveKey}/>
<NavTab eventKey="search" label="Search" icon="search" activeKey={activeKey}
onTabClick={setActiveKey}/>
<NavTab eventKey="playlists" label="Playlists" icon="bookmarks" activeIcon="bookmarks-fill"
activeKey={activeKey} onTabClick={setActiveKey}/>
<NavTab eventKey="spotify-login" label="Spotify Login" icon="spotify" activeIcon="spotify"
activeKey={activeKey}
onTabClick={() => window.open("http://localhost:8080/module/spotify/login", "_blank")}/>
</Nav>
</Container>
</Navbar>
);
};
export default NavbarContainer;

View File

@ -1,14 +1,70 @@
import React from "react"; import React, {useEffect, useState} from "react";
import SearchSongForm from "../components/SearchSongForm";
import api from "../api";
import SearchResultContainer from "./SearchResult.container";
import ExternalTrack from "../model/externalTrack.model";
const audio = new Audio();
const SearchContainer: React.FC = () => { const SearchContainer: React.FC = () => {
const [results, setResults] = useState({});
const [previewTrackId, setPreviewTrackId] = useState("");
const [importedTracksIds, setImportedTracksIds] = useState(new Array<string>());
const submitSearchHandler = async (query: string) => {
const response = await api.get("/tracks/search?q=" + query, {withCredentials: true});
setResults(response.data);
};
const onPreviewClick = (trackId: string, previewUrl: string) => {
audio.pause();
if (trackId !== previewTrackId) {
audio.src = previewUrl;
audio.play();
setPreviewTrackId(trackId);
} else {
setPreviewTrackId("");
}
};
const onImportClick = async (trackId: string) => {
if (importedTracksIds.some(id => trackId === id)) {
await api.delete(`/tracks/trackId/spotify/${trackId}`);
setImportedTracksIds(importedTracksIds.filter(id => trackId !== id));
} else {
await api.post("/tracks/", {source: "spotify", trackId}, {withCredentials: true});
setImportedTracksIds([...importedTracksIds, trackId]);
}
};
const onSpotifyIconClick = (trackId: string) => {
const spotifyTrackUrl = "https://open.spotify.com/track/" + trackId;
window.open(spotifyTrackUrl, "_blank");
};
useEffect(() => {
const getLibraryTrackIds = async () => {
const response = await api.get("/tracks/trackIds/");
if (response.data.spotify) {
setImportedTracksIds(response.data.spotify);
}
};
getLibraryTrackIds();
}, []);
return ( return (
<> <>
<div> <SearchSongForm submitSearchHandler={submitSearchHandler}/>
<form> {Object.entries(results).map(([source, tracks]) => (
<input name="query" type="string" placeholder="Search query" required /> <SearchResultContainer key={source} source={source} importedTracksIds={importedTracksIds}
<button>Search</button> previewTrackId={previewTrackId}
</form> tracks={tracks as ExternalTrack[]}
</div> onImportClick={onImportClick}
onPreviewClick={onPreviewClick}
onSpotifyIconClick={onSpotifyIconClick}/>
))}
</> </>
) )
} }

View File

@ -0,0 +1,44 @@
import {FC} from "react";
import ExternalTrackCard from "../components/ExternalTrackCard";
import ExternalTrack from "../model/externalTrack.model";
import SpotifyLogo from "../images/Spotify_Logo_RGB_White.png";
import "../style/search-results.scss";
const SearchResultContainer: FC<SearchResultContainerProps> = ({
source,
importedTracksIds,
previewTrackId,
tracks,
onImportClick,
onPreviewClick,
onSpotifyIconClick
}) => {
return (
<div className="d-flex flex-column">
<img className="search-results-source-logo" src={SpotifyLogo} alt="Spotify logo"/>
<div className="search-results-wrapper d-flex flex-row">
{tracks.map(s => (
<ExternalTrackCard key={s.trackId} track={s}
source={source}
imported={importedTracksIds.some(id => s.trackId === id)}
previewing={previewTrackId === s.trackId}
onImportClick={() => onImportClick(s.trackId)}
onPreviewClick={() => onPreviewClick(s.trackId, s.previewUrl)}
onSpotifyIconClick={() => onSpotifyIconClick(s.trackId)}/>
))}
</div>
</div>
);
};
interface SearchResultContainerProps {
source: string,
importedTracksIds: string[],
previewTrackId: string,
tracks: ExternalTrack[],
onImportClick: (trackId: string) => void,
onPreviewClick: (trackId: string, previewUrl: string) => void,
onSpotifyIconClick: (trackId: string) => void
}
export default SearchResultContainer;

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -1,9 +1,11 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import './index.css';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import {RouterProvider} from 'react-router-dom'; import {BrowserRouter, Route, Routes} from 'react-router-dom';
import router from './router'; import './style/index.scss';
import NavbarContainer from "./containers/Navbar.container";
import SearchContainer from "./containers/Search.container";
import MainContainer from './containers/Main.container';
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement
@ -11,7 +13,20 @@ const root = ReactDOM.createRoot(
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<RouterProvider router={router}/> <BrowserRouter>
<div className="d-flex flex-row">
<div className="sidebar">
<NavbarContainer/>
</div>
<div className="p-3">
<Routes>
<Route path="/" element={<MainContainer/>}/>
<Route path="/library" element={<MainContainer/>}/>
<Route path="/search" element={<SearchContainer/>}/>
</Routes>
</div>
</div>
</BrowserRouter>
</React.StrictMode> </React.StrictMode>
); );

View File

@ -0,0 +1,41 @@
export default class ExternalTrack {
private readonly _trackId: string;
private readonly _name: string;
private readonly _albumName: string;
private readonly _authors: string[];
private readonly _thumbnailUrl: string;
private readonly _previewUrl: string;
public constructor(trackId: string, name: string, albumName: string, authors: string[], thumbnailUrl: string, previewUrl: string) {
this._trackId = trackId;
this._name = name;
this._albumName = albumName;
this._authors = authors;
this._thumbnailUrl = thumbnailUrl;
this._previewUrl = previewUrl;
}
public get trackId(): string {
return this._trackId;
}
public get name(): string {
return this._name;
}
public get albumName(): string {
return this._albumName;
}
public get authors(): string[] {
return this._authors;
}
public get thumbnailUrl(): string {
return this._thumbnailUrl;
}
public get previewUrl(): string {
return this._previewUrl;
}
}

View File

@ -1,12 +1,12 @@
export default class Song { export default class Track {
private readonly _id: string; private readonly _id: string;
private readonly _songId: string; private readonly _trackId: string;
private readonly _name: string; private readonly _name: string;
private readonly _authors: string[]; private readonly _authors: string[];
public constructor(id: string, songId: string, name: string, authors: string[]) { public constructor(id: string, trackId: string, name: string, authors: string[]) {
this._id = id; this._id = id;
this._songId = songId; this._trackId = trackId;
this._name = name; this._name = name;
this._authors = authors; this._authors = authors;
} }
@ -15,8 +15,8 @@ export default class Song {
return this._id; return this._id;
} }
public get songId(): string { public get trackId(): string {
return this._songId; return this._trackId;
} }
public get name(): string { public get name(): string {

32
src/style/_theme.scss Normal file
View File

@ -0,0 +1,32 @@
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";
/* https://coolors.co/fc8607-8d99ae-edf2f4-29262c-1f1c21 */
$color-primary: #fc8607;
$color-background: #191919;
$color-background-2: darken($color-background, 5%);
$font-color-background: white;
$body-bg: $color-background !default;
$body-color: white;
$navbar-width: 15rem;
$theme-colors: (
"primary": $color-primary,
"secondary": #6c757d,
"success": #28a745,
"danger": #dc3545,
"warning": #ffc107,
"info": #17a2b8,
"light": #f8f9fa,
"dark": $color-background-2
);
//$theme-colors-rgb: map-loop($theme-colors, to-rgb, "$value");
//$utilities-colors: map-merge($utilities-colors, $theme-colors-rgb);
//$utilities-text-colors: map-loop($utilities-colors, rgba-css-var, "$key", "text");
//$utilities-bg-colors: map-loop($utilities-colors, rgba-css-var, "$key", "bg");
@import "~bootstrap/scss/bootstrap";

28
src/style/index.scss Normal file
View File

@ -0,0 +1,28 @@
@import "~bootstrap/scss/bootstrap";
@import "~bootstrap-icons/font/bootstrap-icons";
@import "theme";
body {
margin: 0;
padding: 0;
// font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
// 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
// sans-serif;
font-family: 'Helvetica Neue', sans-serif;
background-color: $color-background !important;
color: $font-color-background;
overflow: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.sidebar {
background-color: $color-background-2;
min-width: 15rem;
height: 100vh;
}

34
src/style/navbar.scss Normal file
View File

@ -0,0 +1,34 @@
@import "theme";
nav {
background-color: $color-background-2;
width: $navbar-width;
.navbar-nav {
width: $navbar-width;
flex-direction: column !important;
color: white;
}
.nav-link {
color: rgba(255, 255, 255, 0.7);
font-weight: bold;
font-size: 1.2em;
display: flex;
flex-direction: row;
align-items: center;
i, span {
color: rgba(255, 255, 255, 0.7);
margin-right: 1em;
}
&:hover i, &:hover span, &.active span {
color: white;
}
&.active i {
color: $color-primary !important;
}
}
}

View File

@ -0,0 +1,59 @@
@import "theme";
$thumbnail-size: 15em;
$wrapper-margin: 1em;
$wrapper-width: calc(100vw - $navbar-width - (2 * $wrapper-margin));
.search-results-wrapper {
overflow-x: scroll;
width: $wrapper-width;
}
.search-results-source-logo {
width: 10em;
margin-top: $wrapper-margin;
margin-left: $wrapper-margin;
}
.external-track-card {
width: $thumbnail-size;
margin: 1em;
img.external-track-thumbnail {
width: $thumbnail-size;
height: $thumbnail-size;
margin-bottom: .5em;
}
.external-track-description {
p {
margin: 0;
}
.external-track-title {
font-weight: bold;
&:hover {
text-decoration: underline;
}
.bi {
margin-left: .25em;
}
}
.external-track-subtitle {
color: rgba(255, 255, 255, 0.7);
line-height: 1.2em;
}
}
.external-track-actions {
font-size: 1.2em;
margin-bottom: 1em;
}
.bi, .external-track-title {
cursor: pointer;
}
}