Changes...
This commit is contained in:
parent
f4fcae38e1
commit
9dacd1944d
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Start Frontend",
|
||||||
|
"type": "command",
|
||||||
|
"program": "npm",
|
||||||
|
"args": [
|
||||||
|
"start"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
38
src/App.css
38
src/App.css
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
17
src/App.tsx
17
src/App.tsx
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: "http://localhost:8080/api/v1",
|
||||||
|
withCredentials: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default api;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 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}/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 |
|
@ -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;
|
|
||||||
}
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
|
@ -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";
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue