diff --git a/.fleet/run.json b/.fleet/run.json new file mode 100644 index 0000000..b87890c --- /dev/null +++ b/.fleet/run.json @@ -0,0 +1,12 @@ +{ + "configurations": [ + { + "name": "Start Frontend", + "type": "command", + "program": "npm", + "args": [ + "start" + ] + } + ] +} \ No newline at end of file diff --git a/.fleet/settings.json b/.fleet/settings.json new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index 8682fbb..2fe0008 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,14 +15,17 @@ "@types/node": "^16.18.10", "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", + "@types/react-router-bootstrap": "^0.24.5", "axios": "^1.2.1", "bootstrap": "^5.2.3", + "bootstrap-icons": "^1.10.3", "react": "^18.2.0", "react-bootstrap": "^2.7.0", "react-dom": "^18.2.0", "react-router-bootstrap": "^0.26.2", "react-router-dom": "^6.6.0", "react-scripts": "5.0.1", + "sass": "^1.57.1", "typescript": "^4.9.4", "web-vitals": "^2.1.4" } @@ -3817,6 +3820,11 @@ "@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": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -3928,6 +3936,34 @@ "@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": { "version": "4.4.5", "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" } }, + "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": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -8748,6 +8789,11 @@ "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": { "version": "3.3.0", "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", "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": { "version": "12.6.0", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", @@ -19642,6 +19704,11 @@ "@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": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -19753,6 +19820,34 @@ "@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": { "version": "4.4.5", "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==", "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": { "version": "1.1.11", "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", "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": { "version": "3.3.0", "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", "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": { "version": "12.6.0", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", diff --git a/package.json b/package.json index a6c1659..e95e4df 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,17 @@ "@types/node": "^16.18.10", "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", + "@types/react-router-bootstrap": "^0.24.5", "axios": "^1.2.1", "bootstrap": "^5.2.3", + "bootstrap-icons": "^1.10.3", "react": "^18.2.0", "react-bootstrap": "^2.7.0", "react-dom": "^18.2.0", "react-router-bootstrap": "^0.26.2", "react-router-dom": "^6.6.0", "react-scripts": "5.0.1", + "sass": "^1.57.1", "typescript": "^4.9.4", "web-vitals": "^2.1.4" }, diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 74b5e05..0000000 --- a/src/App.css +++ /dev/null @@ -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); - } -} diff --git a/src/App.tsx b/src/App.tsx index 5f1bec8..9de7f2f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,21 +1,20 @@ -import axios from 'axios'; import React, {useEffect, useState} from 'react'; -import './App.css'; -import SongDisplay from './components/SongDisplay'; -import Song from './model/song.model'; +import TrackDisplay from './components/TrackDisplay'; +import api from "./api"; +import Track from './model/track.model'; const App: React.FC = () => { - const [songs, setSongs] = useState(new Array()); + const [tracks, setTracks] = useState(new Array()); useEffect(() => { - axios.get("http://localhost:8080/api/v1/song/") - .then(response => setSongs(response.data)); + api.get("/tracks/") + .then(response => setTracks(response.data)); }, []); return (
- {songs.map(s => ( - + {tracks.map(t => ( + ))}
); diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..b849c08 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,8 @@ +import axios from "axios"; + +const api = axios.create({ + baseURL: "http://localhost:8080/api/v1", + withCredentials: true +}); + +export default api; \ No newline at end of file diff --git a/src/components/ExternalTrackCard.tsx b/src/components/ExternalTrackCard.tsx new file mode 100644 index 0000000..89d781c --- /dev/null +++ b/src/components/ExternalTrackCard.tsx @@ -0,0 +1,43 @@ +import React, {FC} from "react"; +import ExternalTrack from "../model/externalTrack.model"; +import Icon from "./Icon"; + +const ExternalTrackCard: FC = ({ + track, + source, + imported, + previewing, + onImportClick, + onPreviewClick, + onSpotifyIconClick + }) => ( +
+ {'Thumbnail +
+

{track.name}

+

{track.authors.join(', ') + ' - ' + track.albumName}

+
+
+ {previewing ? + : + } + + {imported ? + : + } +
+
+); + +interface ExternalTrackCardProps { + track: ExternalTrack, + source: string, + imported: boolean, + previewing: boolean, + onImportClick: () => void, + onPreviewClick: () => void, + onSpotifyIconClick: () => void +} + +export default ExternalTrackCard; \ No newline at end of file diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx new file mode 100644 index 0000000..0fd5ab2 --- /dev/null +++ b/src/components/Icon.tsx @@ -0,0 +1,35 @@ +import {FC, useEffect, useState} from "react"; + +const Icon: FC = ({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 ( + setHovering(true)} + onMouseLeave={() => setHovering(false)} + onClick={onClick}> + ); +}; + +interface IconProps { + name: string + hover?: string + title?: string + onClick?: () => void +} + +export default Icon; \ No newline at end of file diff --git a/src/components/NavTab.tsx b/src/components/NavTab.tsx new file mode 100644 index 0000000..dc27bb4 --- /dev/null +++ b/src/components/NavTab.tsx @@ -0,0 +1,27 @@ +import React, {FC} from "react"; +import {Nav} from "react-bootstrap"; +import {Link} from "react-router-dom"; + +const NavTab: FC = ({eventKey, label, icon, activeIcon, activeKey, onTabClick}) => { + const isActive = () => activeKey === eventKey; + + return ( + onTabClick(eventKey)}> + {!isActive() || activeIcon == null ? + : + } + {label} + + ); +}; + +interface NavTabProps { + eventKey: string, + label: string, + icon: string, + activeIcon?: string, + activeKey: string, + onTabClick: (key: string) => void +} + +export default NavTab; \ No newline at end of file diff --git a/src/components/SearchSongForm.tsx b/src/components/SearchSongForm.tsx new file mode 100644 index 0000000..6358493 --- /dev/null +++ b/src/components/SearchSongForm.tsx @@ -0,0 +1,29 @@ +import {FC, FormEvent, useState} from "react"; + +const SearchSongForm: FC = ({submitSearchHandler}) => { + const [searchQuery, setSearchQuery] = useState(""); + + const submitFormHandler = (e: FormEvent) => { + e.preventDefault(); + submitSearchHandler(searchQuery); + }; + + return ( +
+ setSearchQuery(e.currentTarget.value)}/> + +
+ ); +}; + +interface SearchSongFormProps { + submitSearchHandler: (query: string) => void +} + +export default SearchSongForm; \ No newline at end of file diff --git a/src/components/SongDisplay.tsx b/src/components/SongDisplay.tsx deleted file mode 100644 index a560274..0000000 --- a/src/components/SongDisplay.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -import Song from "../model/song.model"; - -const SongDisplay: React.FC = ({song}) => { - return ( -
-

{song.name} - {song.authors.join(" and ")}

-
- ); -}; - -export interface SongDisplayProps { - song: Song -} - -export default SongDisplay; \ No newline at end of file diff --git a/src/components/TrackDisplay.tsx b/src/components/TrackDisplay.tsx new file mode 100644 index 0000000..7386a78 --- /dev/null +++ b/src/components/TrackDisplay.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import Track from "../model/track.model"; + +const TrackDisplay: React.FC = ({track}) => { + return ( +
+

{track.name} - {track.authors.join(" and ")}

+
+ ); +}; + +export interface TrackDisplayProps { + track: Track +} + +export default TrackDisplay; \ No newline at end of file diff --git a/src/containers/Navbar.container.tsx b/src/containers/Navbar.container.tsx new file mode 100644 index 0000000..2cc8a41 --- /dev/null +++ b/src/containers/Navbar.container.tsx @@ -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 ( + + + + + + ); +}; + +export default NavbarContainer; \ No newline at end of file diff --git a/src/containers/Search.container.tsx b/src/containers/Search.container.tsx index 42d6203..a2680f2 100644 --- a/src/containers/Search.container.tsx +++ b/src/containers/Search.container.tsx @@ -1,15 +1,71 @@ -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 [results, setResults] = useState({}); + const [previewTrackId, setPreviewTrackId] = useState(""); + const [importedTracksIds, setImportedTracksIds] = useState(new Array()); + + 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 ( - <> -
-
- - -
-
- + <> + + {Object.entries(results).map(([source, tracks]) => ( + + ))} + ) } diff --git a/src/containers/SearchResult.container.tsx b/src/containers/SearchResult.container.tsx new file mode 100644 index 0000000..d42f59f --- /dev/null +++ b/src/containers/SearchResult.container.tsx @@ -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 = ({ + source, + importedTracksIds, + previewTrackId, + tracks, + onImportClick, + onPreviewClick, + onSpotifyIconClick + }) => { + return ( +
+ Spotify logo +
+ {tracks.map(s => ( + s.trackId === id)} + previewing={previewTrackId === s.trackId} + onImportClick={() => onImportClick(s.trackId)} + onPreviewClick={() => onPreviewClick(s.trackId, s.previewUrl)} + onSpotifyIconClick={() => onSpotifyIconClick(s.trackId)}/> + ))} +
+
+ ); +}; + +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; \ No newline at end of file diff --git a/src/images/Spotify_Icon_RGB_Black.png b/src/images/Spotify_Icon_RGB_Black.png new file mode 100755 index 0000000..4a83bfd Binary files /dev/null and b/src/images/Spotify_Icon_RGB_Black.png differ diff --git a/src/images/Spotify_Icon_RGB_Green.png b/src/images/Spotify_Icon_RGB_Green.png new file mode 100755 index 0000000..26410e2 Binary files /dev/null and b/src/images/Spotify_Icon_RGB_Green.png differ diff --git a/src/images/Spotify_Icon_RGB_White.png b/src/images/Spotify_Icon_RGB_White.png new file mode 100755 index 0000000..44d781f Binary files /dev/null and b/src/images/Spotify_Icon_RGB_White.png differ diff --git a/src/images/Spotify_Logo_RGB_White.png b/src/images/Spotify_Logo_RGB_White.png new file mode 100755 index 0000000..fce40d8 Binary files /dev/null and b/src/images/Spotify_Logo_RGB_White.png differ diff --git a/src/index.css b/src/index.css deleted file mode 100644 index ec2585e..0000000 --- a/src/index.css +++ /dev/null @@ -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; -} diff --git a/src/index.tsx b/src/index.tsx index 8d131b9..7a3f7be 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,9 +1,11 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import './index.css'; import reportWebVitals from './reportWebVitals'; -import {RouterProvider} from 'react-router-dom'; -import router from './router'; +import {BrowserRouter, Route, Routes} from 'react-router-dom'; +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( document.getElementById('root') as HTMLElement @@ -11,7 +13,20 @@ const root = ReactDOM.createRoot( root.render( - + +
+
+ +
+
+ + }/> + }/> + }/> + +
+
+
); diff --git a/src/model/externalTrack.model.ts b/src/model/externalTrack.model.ts new file mode 100644 index 0000000..ee43a19 --- /dev/null +++ b/src/model/externalTrack.model.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/model/song.model.ts b/src/model/track.model.ts similarity index 61% rename from src/model/song.model.ts rename to src/model/track.model.ts index c1cfd24..1e03664 100644 --- a/src/model/song.model.ts +++ b/src/model/track.model.ts @@ -1,12 +1,12 @@ -export default class Song { +export default class Track { private readonly _id: string; - private readonly _songId: string; + private readonly _trackId: string; private readonly _name: 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._songId = songId; + this._trackId = trackId; this._name = name; this._authors = authors; } @@ -15,8 +15,8 @@ export default class Song { return this._id; } - public get songId(): string { - return this._songId; + public get trackId(): string { + return this._trackId; } public get name(): string { diff --git a/src/style/_theme.scss b/src/style/_theme.scss new file mode 100644 index 0000000..e2fcca6 --- /dev/null +++ b/src/style/_theme.scss @@ -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"; \ No newline at end of file diff --git a/src/style/index.scss b/src/style/index.scss new file mode 100644 index 0000000..19c98bd --- /dev/null +++ b/src/style/index.scss @@ -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; +} \ No newline at end of file diff --git a/src/style/navbar.scss b/src/style/navbar.scss new file mode 100644 index 0000000..2bfd3d5 --- /dev/null +++ b/src/style/navbar.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/src/style/search-results.scss b/src/style/search-results.scss new file mode 100644 index 0000000..ed2400a --- /dev/null +++ b/src/style/search-results.scss @@ -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; + } +} \ No newline at end of file