diff --git a/controllers/404-controller.js b/controllers/404-controller.js new file mode 100644 index 00000000..6895af84 --- /dev/null +++ b/controllers/404-controller.js @@ -0,0 +1,3 @@ +module.exports.get404Page = (req, res) => { + res.status(404).render('404'); +} \ No newline at end of file diff --git a/controllers/artists-controller.js b/controllers/artists-controller.js new file mode 100644 index 00000000..23f3fd29 --- /dev/null +++ b/controllers/artists-controller.js @@ -0,0 +1,76 @@ +const db = require('../util/database.js'); +const Artist = require('../models/artist.js'); + +module.exports.getArtistById = async (req, res) => { + + const { id } = req.params; + + const rows = await Artist.get(id); + + res.render('./artists/artist-single', { 'singleArtist': rows[0] }); + +} + +module.exports.getAllArtists = async (req, res) => { + + const rows = await Artist.getAll(); + + res.render('./artists/artist-all', { 'allArtists': rows }); + +} + +module.exports.getAddArtist = async (req, res) => { + res.render('./artists/add-artist'); +} + +module.exports.postAddArtist = async (req, res) => { + + const newArtist = new Artist( + req.body.name, + req.body.nationality, + req.body['image link']); + + console.log(newArtist); + + const rows = await newArtist.save(); + + res.redirect(`./artists/${rows[0].id}`); + +} + +module.exports.getEditArtistById = async (req, res) => { + + const { id } = req.params; + + const rows = await Artist.get(id); + + res.render('./artists/edit-artist', { + 'singleArtist': rows[0] + }); + +} + +module.exports.putArtistById = async (req, res) => { + + + const { id } = req.params; + + await Artist.edit( + req.body.name, + req.body.nationality, + req.body['image link'], + id); + + res.redirect(`./${id}`); + +} + +module.exports.deleteArtistById = async (req, res) => { + + const { id } = req.params; + + await Artist.delete(id); + + res.render('./artists/artist-deleted'); + +} \ No newline at end of file diff --git a/controllers/auth-controller.js b/controllers/auth-controller.js new file mode 100644 index 00000000..70e42b81 --- /dev/null +++ b/controllers/auth-controller.js @@ -0,0 +1,171 @@ +const db = require('../util/database.js'); +const sha256 = require('js-sha256'); +const User = require('../models/user.js'); + + +module.exports.getLoginRegister = async (req, res) => { + + req.session.invalidMsg = ""; + + res.render('./auth/login-register', { singleArtist: req.featuredArtist }); + +} + +module.exports.getLogin = async (req, res) => { + + res.render('./auth/login', { invalidMsg: req.session.invalidMsg }); + +} + +module.exports.getRegister = async (req, res) => { + + res.render('./auth/register', { invalidMsg: req.session.invalidMsg }); + +} + +module.exports.postLogin = async (req, res) => { + + const { email, password } = req.body; + + if (!email || !password) { + + req.session.invalidMsg = 'Please enter user email and password'; + + res.redirect('./login'); + } + + const queryT1 = `SELECT * FROM users WHERE email ='${email}'` + const { rows } = await db.query(queryT1); + + if (!rows[0]) { + + req.session.invalidMsg = 'Email is not registered' + + res.redirect('./login'); + + } else if (rows[0].password !== sha256(password)) { + + req.session.invalidMsg = 'Wrong password' + + res.redirect('./login'); + + } else if (rows[0]['email'] == email && rows[0]['password'] == sha256(password)) { + + if (req.cookies.userId && req.cookies.visits) { + const queryT2 = `UPDATE users SET visits=${req.cookies.visits} WHERE id=${req.cookies.userId}` + await db.query(queryT2); + } + + req.session.userId = rows[0].id; + req.session.invalidMsg = ""; + res.clearCookie('visits'); + res.clearCookie('userId'); + console.log(req.session.userId); + res.redirect('/'); + } +} + +module.exports.postRegister = async (req, res) => { + + const { email, password } = req.body; + + if (!email || !password) { + + req.session.invalidMsg = 'Please enter your email and password'; + + res.redirect('./register'); + } + + const queryT1 = `SELECT * FROM users` + const { rows } = await db.query(queryT1); + + + if (email && password) { + const userExists = rows.some( + usr => usr['email'] == email + ) + + if (!userExists) { + const newUser = new User(email, sha256(password)); + + const queryT2 = `INSERT INTO users (email, password) VALUES($1, $2)`; + const queryV2 = [newUser.email, newUser.password]; + + await db.query(queryT2, queryV2); + + const queryT3 = `SELECT * FROM users WHERE email='${newUser.email}'`; + const resultThree = await db.query(queryT3); + + req.session.userId = resultThree.rows[0].id; + + res.redirect('/'); + + } else { + + req.session.invalidMsg = 'User already exists. Please Login.'; + + res.redirect('./login'); + + } + } + +} + +module.exports.postLogout = async (req, res) => { + + if (req.cookies.userId && req.cookies.visits) { + + const queryT = `UPDATE users SET visits=${req.cookies.visits} WHERE id=${req.cookies.userId}`; + + await db.query(queryT); + + } + + req.session.destroy(); + res.clearCookie('userId'); + res.clearCookie('visits'); + res.clearCookie('sid'); + res.redirect('/'); + +} + +module.exports.getUserInfo = async (userId) => { + + const queryT = `SELECT * FROM users WHERE id=${userId}`; + + const { rows } = await db.query(queryT); + + console.log(`Currently Logged In as ${rows[0].email}`) + + return rows[0]; +} + +module.exports.visitsCookieCounter = async (req, res) => { + + let visits = parseInt(req.cookies['visits']); + + if (!visits) { + + if (req.cookies['userId']) { + + const queryT = `SELECT id, visits FROM users WHERE id=${req.cookies['userId']}`; + + const { rows } = await db.query(queryT); + + visits = parseInt(rows[0]['visits']) + 1; + + } else { + + return; + + } + + } else { + + visits = parseInt(visits) + 1; + + } + + res.cookie('visits', visits); + +} \ No newline at end of file diff --git a/controllers/favourites-controller.js b/controllers/favourites-controller.js new file mode 100644 index 00000000..3c260cab --- /dev/null +++ b/controllers/favourites-controller.js @@ -0,0 +1,69 @@ +const db = require('../util/database.js'); + +module.exports.postFavourites = async (req, res) => { + + const queryT1 = `SELECT * FROM favourites WHERE user_id=${req.currentUser.id}`; + + const { rows } = await db.query(queryT1); + + console.log(rows); + + const songAlrInFavs = rows.some(log => { + return log['song_id'] == req.body.songId; + }) + + if (songAlrInFavs) { + + return; + + } else { + + const queryT2 = `INSERT INTO favourites (user_id, song_id) VALUES($1, $2)`; + const queryV2 = [req.currentUser.id, req.body.songId]; + + const resultTwo = await db.query(queryT2, queryV2); + + console.log(resultTwo['rows']); + + } +} + +module.exports.getAllFavourites = async (req, res) => { + + const queryT = `SELECT * FROM favourites WHERE user_id = ${req.currentUser.id} ORDER BY id`; + + const { rows } = await db.query(queryT); + + + if (rows.length == 0) { + res.render('./favourites/favourites-all', { + favouriteSongs: "", + currentUser: req['currentUser'] + }) + } else { + const favouriteSongsIdArr = rows + .reduce((acc, log) => { + acc.push(log['song_id']); + return acc; + }, []) + + const queryT2 = `SELECT * FROM songs WHERE id IN (${favouriteSongsIdArr.toString()})`; + const resultTwo = await db.query(queryT2); + const favouriteSongsArr = resultTwo.rows; + + res.render('./favourites/favourites-all', { + favouriteSongs: favouriteSongsArr, + currentUser: req['currentUser'] + }) + } + + +} + +module.exports.deleteFavourites = async (req, res) => { + const queryT = `DELETE FROM favourites WHERE user_id = ${req.currentUser.id} AND song_id = ${req.body.songId}`; + + await db.query(queryT); + + res.redirect('./'); +} \ No newline at end of file diff --git a/controllers/playlists-controller.js b/controllers/playlists-controller.js new file mode 100644 index 00000000..83bcf223 --- /dev/null +++ b/controllers/playlists-controller.js @@ -0,0 +1,135 @@ +const db = require('../util/database.js'); +const Playlist = require('../models/playlist.js'); +const getDateUtil = require('../util/get-date.js'); + +module.exports.getPlaylistById = async (req, res) => { + + const { id } = req.params; + + const rows = await Playlist.get(id); + + try { + + res.render('./playlists/playlist-single', { 'singlePlaylist': rows }); + + } catch (e) { + + res.status(404).render('404'); + console.log(e); + + } + +} + +module.exports.getAllPlaylists = async (req, res) => { + + const rows = await Playlist.getAll(); + + try { + + res.render('./playlists/playlist-all', { 'allPlaylists': rows }); + + } catch (e) { + + res.status(404).render('404'); + console.log(e); + + } + +} + +module.exports.getAddPlaylist = async (req, res) => { + + const queryT = `SELECT name, id from artists`; + const queryT2 = `SELECT title, artist_id from songs`; + const resultArtists = await db.query(queryT); + const resultSongs = await db.query(queryT2); + + res.render('./playlists/add-playlist', { + artists: resultArtists.rows, + songs: resultSongs.rows + }); +} + +module.exports.postAddPlaylist = async (req, res) => { + + const newPlaylist = new Playlist(req.body['playlist_name']); + + const rows = await newPlaylist.save(req.body); + + res.redirect(`./playlists/${rows[0].id}`); + +} + +module.exports.getEditPlaylistById = async (req, res) => { + + const { id } = req.params; + const queryT = `SELECT * FROM playlists WHERE id=${id}` + const { rows } = await db.query(queryT); + + const queryT2 = `SELECT song_id, title, artist_id from playlists_songs INNER JOIN songs ON playlists_songs.song_id = songs.id WHERE playlist_id=${id} ORDER BY song_id` + + const resultTwo = await db.query(queryT2); + + const queryT3 = `SELECT name, id from artists`; + const queryT4 = `SELECT title, artist_id from songs`; + const resultArtists = await db.query(queryT3); + const resultSongs = await db.query(queryT4); + + res.render('./playlists/edit-playlist', { + 'singlePlaylist': rows[0], + 'playlistSongs': resultTwo.rows, + 'artists': resultArtists.rows, + 'songs': resultSongs.rows + }) + +} + +module.exports.putPlaylistById = async (req, res) => { + + const { id } = req.params; + const queryT = `UPDATE playlists SET name = '${req.body.name}' WHERE id=${id}` + const { rows } = await db.query(queryT); + + if (!Array.isArray(req.body.artist)) + req.body.artist = req.body.artist.split(); + + if (!Array.isArray(req.body.song)) + req.body.song = req.body.song.split(); + + let i = 0; + const playlistArr = req.body.artist + .reduce((arr, el) => { + const obj = { 'artist': el, 'song': req.body.song[i] }; + i++; + return arr.push(obj), arr; + }, []) + + const queryT2 = `DELETE FROM playlists_songs where playlist_id =${id}` + + const resultTwo = await db.query(queryT2); + + playlistArr.forEach(async playlistSong => { + const queryT3 = `SELECT id, artist_id FROM songs WHERE title='${playlistSong.song}'`; + const { rows } = await db.query(queryT3); + + const queryT4 = `INSERT into playlists_songs(song_id, playlist_id) VALUES ('${rows[0].id}', '${id}') RETURNING *` + + const resultFour = await db.query(queryT4); + + }) + + res.redirect(`./${id}`); + +} + +module.exports.deletePlaylistById = async (req, res) => { + + const { id } = req.params; + const queryT = `DELETE from playlists WHERE id=${id}` + const { rows } = await db.query(queryT); + res.render('./playlists/playlist-deleted'); + + console.log('Delete Playlist By Id'); + +} \ No newline at end of file diff --git a/controllers/songs-controller.js b/controllers/songs-controller.js new file mode 100644 index 00000000..673e50b9 --- /dev/null +++ b/controllers/songs-controller.js @@ -0,0 +1,80 @@ +const db = require('../util/database.js'); +const Song = require('../models/song.js'); + +module.exports.getArtistSongById = async (req, res) => { + + res.render('./songs/song-single.jsx', { 'singleSong': req.song }); + +} + +module.exports.getAllSongsOfArtist = async (req, res) => { + + try { + + const rows = await Song.getAll(req.artist.id); + + console.log(req.currentUser); + + res.render('./songs/song-all.jsx', { + 'allSongs': rows, + 'artist': req.artist, + 'currentUser': req.currentUser + }); + + } catch (e) { + res.status(404).render('404'); + console.log(e); + } +} + +module.exports.getAddSongToArtist = async (req, res) => { + + res.render('./songs/add-song.jsx', { 'artist': req.artist }); +} + +module.exports.postAddSongToArtist = async (req, res) => { + + const newSong = new Song( + req.body.title, + req.body.album, + req.body["preview link"], + req.body.artwork, + req.artist.id); + + await newSong.save(); + + res.redirect('./songs'); + +} + +module.exports.getEditArtistSongById = async (req, res) => { + + !req.song.title ? res.render('404') : res.render('./songs/edit-song.jsx', { + 'singleSong': req.song, + 'artist': req.artist + }); + +} + +module.exports.putArtistSongById = async (req, res) => { + + await Song.edit( + req.body.title, + req.body.album, + req.body['preview link'], + req.body.artwork, + req.artist.id, + req.song.id + ); + + res.redirect(`./${req.song.position}`); + +} + +module.exports.deleteArtistSongById = async (req, res) => { + + await Song.delete(req.song.id); + + res.render('./songs/song-deleted'); + +} \ No newline at end of file diff --git a/artist_data.sql b/db/artist_data.sql similarity index 77% rename from artist_data.sql rename to db/artist_data.sql index 8f0cf579..b62c5abc 100644 --- a/artist_data.sql +++ b/db/artist_data.sql @@ -1,5 +1,6 @@ -INSERT INTO artists(name, photo_url, nationality) VALUES('Yeah Yeah Yeahs', 'http://www.athousandguitars.com/wp-content/uploads/2013/04/yeah-yeah-yeahs.jpg', 'USA'); +----psql -U zachariah -d tunr_db -a -f ./db/artist_data.sql-- +INSERT INTO artists(name, photo_url, nationality) VALUES('Yeah Yeah Yeahs', 'https://www.allthingsgomusic.com/wp-content/uploads/2017/09/Yeah-Yeah-Yeahs-1.jpg', 'USA'); INSERT INTO artists(name, photo_url, nationality) VALUES('Nosaj Thing', 'http://wertn.com/wp-content/uploads/2012/04/Nosaj-Thing_Mondrian_CL_High-3487.jpg', 'USA'); INSERT INTO artists(name, photo_url, nationality) VALUES('Norah Jones', 'http://entertainmentrealm.files.wordpress.com/2012/05/norahjones1.jpg', 'USA'); INSERT INTO artists(name, photo_url, nationality) VALUES('Lykke Li', 'http://www.chartattack.com/wp-content/uploads/2012/07/lykke-li-newmain1-photo-by-daniel-jackson.jpg', 'Sweeden'); -INSERT INTO artists(name, photo_url, nationality) VALUES('Kendrick Lamar', 'http://www.xxlmag.com/wp-content/uploads/2013/06/kendricklamar_001-1600.jpg', 'USA'); +INSERT INTO artists(name, photo_url, nationality) VALUES('Kendrick Lamar', 'http://www.xxlmag.com/wp-content/uploads/2013/06/kendricklamar_001-1600.jpg', 'USA'); \ No newline at end of file diff --git a/songs.sql b/db/songs.sql similarity index 99% rename from songs.sql rename to db/songs.sql index 9877ce48..4c060122 100644 --- a/songs.sql +++ b/db/songs.sql @@ -1,3 +1,4 @@ +--psql -U zachariah -d tunr_db -a -f ./db/songs.sql-- INSERT INTO songs(title, album, preview_link, artwork, artist_id) VALUES('Maps', 'Fever to Tell', 'http://a1748.phobos.apple.com/us/r1000/074/Music/d4/97/e7/mzm.bigdtgoz.aac.p.m4a', 'http://a3.mzstatic.com/us/r30/Features/d6/ba/99/dj.homcvzwl.60x60-50.jpg', '1'); INSERT INTO songs(title, album, preview_link, artwork, artist_id) VALUES('Heads Will Roll', 'It''s Blitz! (Deluxe Edition)', 'http://a308.phobos.apple.com/us/r1000/064/Music/9c/a6/3a/mzm.zgjjoqyj.aac.p.m4a', 'http://a1.mzstatic.com/us/r30/Music/4c/30/8c/mzi.gcicgujl.60x60-50.jpg', '1'); INSERT INTO songs(title, album, preview_link, artwork, artist_id) VALUES('Gold Lion', 'Show Your Bones', 'http://a850.phobos.apple.com/us/r1000/105/Music/d0/b6/fe/mzm.qoeoeazp.aac.p.m4a', 'http://a5.mzstatic.com/us/r30/Features/73/a1/1a/dj.mwlaurzf.60x60-50.jpg', '1'); @@ -247,4 +248,4 @@ INSERT INTO songs(title, album, preview_link, artwork, artist_id) VALUES('Bitch, Don’t Kill My Vibe (feat. JAY Z)', 'good kid, m.A.A.d city', 'http://a796.phobos.apple.com/us/r1000/081/Music/v4/64/05/93/640593c0-7a02-3082-e539-a3928c1b3fcf/mzaf_5043431582450077336.aac.m4a', 'http://a4.mzstatic.com/us/r30/Music2/v4/fe/14/11/fe1411bd-77a8-0f69-824a-20145345d9a8/UMG_cvrart_00602537362783_01_RGB72_1500x1500_12UMGIM52988.60x60-50.jpg', '5'); INSERT INTO songs(title, album, preview_link, artwork, artist_id) VALUES('Swimming Pools (Drank)', 'good kid, m.A.A.d city', 'http://a1265.phobos.apple.com/us/r1000/063/Music/v4/12/0f/13/120f13b3-ba47-2223-d97a-f9ddeae408b6/mzaf_1783459415414390232.aac.m4a', 'http://a1.mzstatic.com/us/r30/Music2/v4/53/ea/bb/53eabb9c-dfaf-b926-cc73-9a1ad6ee548f/UMG_cvrart_00602537362790_01_RGB72_1500x1500_12UMGIM52989.60x60-50.jpg', '5'); INSERT INTO songs(title, album, preview_link, artwork, artist_id) VALUES('Michael Jordan (feat. Schoolboy Q)', 'Overly Dedicated', 'http://a372.phobos.apple.com/us/r30/Music/ff/b3/1a/mzm.ycpsoiyo.aac.p.m4a', 'http://a2.mzstatic.com/us/r30/Music/7f/4a/12/mzi.onkyvccv.60x60-50.jpg', '5'); - INSERT INTO songs(title, album, preview_link, artwork, artist_id) VALUES('Sing About Me, I''m Dying of Thirst', 'good kid, m.A.A.d city', 'http://a731.phobos.apple.com/us/r1000/081/Music/v4/b8/61/36/b861368e-1960-5b0a-63b7-8cdfc5e0c802/mzaf_9127169611152605592.aac.m4a', 'http://a1.mzstatic.com/us/r30/Music2/v4/53/ea/bb/53eabb9c-dfaf-b926-cc73-9a1ad6ee548f/UMG_cvrart_00602537362790_01_RGB72_1500x1500_12UMGIM52989.60x60-50.jpg', '5'); + INSERT INTO songs(title, album, preview_link, artwork, artist_id) VALUES('Sing About Me, I''m Dying of Thirst', 'good kid, m.A.A.d city', 'http://a731.phobos.apple.com/us/r1000/081/Music/v4/b8/61/36/b861368e-1960-5b0a-63b7-8cdfc5e0c802/mzaf_9127169611152605592.aac.m4a', 'http://a1.mzstatic.com/us/r30/Music2/v4/53/ea/bb/53eabb9c-dfaf-b926-cc73-9a1ad6ee548f/UMG_cvrart_00602537362790_01_RGB72_1500x1500_12UMGIM52989.60x60-50.jpg', '5'); \ No newline at end of file diff --git a/db/table-artist_data.sql b/db/table-artist_data.sql new file mode 100644 index 00000000..cac86214 --- /dev/null +++ b/db/table-artist_data.sql @@ -0,0 +1,8 @@ +--psql -U zachariah -d tunr_db -a -f ./db/table-artist_data.sql-- + +CREATE TABLE artists ( + id serial PRIMARY KEY, + name VARCHAR (255) UNIQUE NOT NULL, + photo_url VARCHAR (255), + nationality VARCHAR (255) +); \ No newline at end of file diff --git a/db/table-favorites.sql b/db/table-favorites.sql new file mode 100644 index 00000000..ddff68fd --- /dev/null +++ b/db/table-favorites.sql @@ -0,0 +1,5 @@ +CREATE TABLE favourites ( + id serial PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + song_id INTEGER REFERENCES songs(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/db/table-playlists.sql b/db/table-playlists.sql new file mode 100644 index 00000000..815c035f --- /dev/null +++ b/db/table-playlists.sql @@ -0,0 +1,5 @@ +CREATE TABLE playlists ( + id serial PRIMARY KEY, + created_on TIMESTAMP, + name VARCHAR (255) NOT NULL +); \ No newline at end of file diff --git a/db/table-playlists_songs.sql b/db/table-playlists_songs.sql new file mode 100644 index 00000000..4da98978 --- /dev/null +++ b/db/table-playlists_songs.sql @@ -0,0 +1,5 @@ +CREATE TABLE playlists_songs ( + id serial PRIMARY KEY, + playlist_id INTEGER REFERENCES playlists(id) ON DELETE CASCADE, + song_id INTEGER REFERENCES songs(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/db/table-songs.sql b/db/table-songs.sql new file mode 100644 index 00000000..00a499af --- /dev/null +++ b/db/table-songs.sql @@ -0,0 +1,10 @@ +--psql -U zachariah -d tunr_db -a -f ./db/table-songs.sql-- + +CREATE TABLE songs ( + id serial PRIMARY KEY, + title VARCHAR (255) NOT NULL, + album VARCHAR (255), + preview_link TEXT, + artwork TEXT, + artist_id INTEGER REFERENCES artists(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/db/table-users.sql b/db/table-users.sql new file mode 100644 index 00000000..cd9447f2 --- /dev/null +++ b/db/table-users.sql @@ -0,0 +1,8 @@ +CREATE EXTENSION CITEXT; + +CREATE TABLE users ( + id serial PRIMARY KEY, + email CITEXT NOT NULL, + password VARCHAR (64) NOT NULL, + visits INTEGER DEFAULT 0 +); \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index c5a34e0a..00000000 --- a/index.js +++ /dev/null @@ -1,84 +0,0 @@ -console.log("starting up!!"); - -const express = require('express'); -const methodOverride = require('method-override'); -const pg = require('pg'); - -// Initialise postgres client -const configs = { - user: 'YOURUSERNAME', - host: '127.0.0.1', - database: 'tunr_db', - port: 5432, -}; - -const pool = new pg.Pool(configs); - -pool.on('error', function (err) { - console.log('idle client error', err.message, err.stack); -}); - -/** - * =================================== - * Configurations and set up - * =================================== - */ - -// Init express app -const app = express(); - - -app.use(express.json()); -app.use(express.urlencoded({ - extended: true -})); - -app.use(methodOverride('_method')); - - -// Set react-views to be the default view engine -const reactEngine = require('express-react-views').createEngine(); -app.set('views', __dirname + '/views'); -app.set('view engine', 'jsx'); -app.engine('jsx', reactEngine); - -/** - * =================================== - * Routes - * =================================== - */ - -app.get('/', (request, response) => { - // query database for all pokemon - - // respond with HTML page displaying all pokemon - response.render('home'); -}); - -app.get('/new', (request, response) => { - // respond with HTML page with form to create new pokemon - response.render('new'); -}); - - -/** - * =================================== - * Listen to requests on port 3000 - * =================================== - */ -const server = app.listen(3000, () => console.log('~~~ Tuning in to the waves of port 3000 ~~~')); - -let onClose = function(){ - - console.log("closing"); - - server.close(() => { - - console.log('Process terminated'); - - pool.end( () => console.log('Shut down db connection pool')); - }) -}; - -process.on('SIGTERM', onClose); -process.on('SIGINT', onClose); diff --git a/models/artist.js b/models/artist.js new file mode 100644 index 00000000..3ac2665a --- /dev/null +++ b/models/artist.js @@ -0,0 +1,58 @@ +const db = require('../util/database.js'); + +module.exports = class Artist { + + constructor(name, nationality, photo) { + + this.name = name; + this.nationality = nationality; + this.photo = photo; + + } + + async save() { + + const queryT = `INSERT INTO artists(name, nationality, photo_url) VALUES($1, $2, $3) RETURNING *`; + const queryV = [this.name, this.nationality, this.photo]; + + const { rows } = await db.query(queryT, queryV); + + return rows; + + } + + static async get(artistId) { + + const queryT = `SELECT * FROM artists WHERE id=${artistId}` + const { rows } = await db.query(queryT); + + return rows; + + } + + static async getAll() { + + const { rows } = await db.query('SELECT * FROM artists'); + + return rows; + + } + + static async edit(name, nationality, photo, artistId) { + + const queryT = `UPDATE artists SET name = '${name}', nationality = '${nationality}', photo_url = '${photo}' WHERE id=${artistId}` + const { rows } = await db.query(queryT); + + return rows; + + } + + static async delete(artistId) { + + const queryT = `DELETE from artists WHERE id=${artistId}` + const { rows } = await db.query(queryT); + + return rows; + + } +} \ No newline at end of file diff --git a/models/playlist.js b/models/playlist.js new file mode 100644 index 00000000..6d61648d --- /dev/null +++ b/models/playlist.js @@ -0,0 +1,66 @@ +const db = require('../util/database.js'); +const getDateUtil = require('../util/get-date.js'); + +module.exports = class Playlist { + + constructor(name) { + this.name = name; + } + + async save(reqBody) { + + if (!Array.isArray(reqBody.artist)) + reqBody.artist = reqBody.artist.split(); + + if (!Array.isArray(reqBody.song)) + reqBody.song = reqBody.song.split(); + + let i = 0; + const playlistArr = reqBody.artist + .reduce((arr, el) => { + + const obj = { 'artist': el, 'song': reqBody.song[i] }; + i++; + return arr.push(obj), arr; + + }, []) + + const queryT1 = `INSERT into playlists(created_on, name) VALUES ($1, $2) RETURNING *` + const queryV1 = [getDateUtil.getTimeStamp(), this.name]; + + const resultOne = await db.query(queryT1, queryV1); + + playlistArr.forEach(async playlistSong => { + + const queryT2 = `SELECT id, artist_id FROM songs WHERE title='${playlistSong.song}'`; + const { rows } = await db.query(queryT2); + + const queryT3 = `INSERT into playlists_songs(song_id, playlist_id) VALUES ('${rows[0].id}', '${resultOne.rows[0].id}') RETURNING *` + const resultThree = await db.query(queryT3); + + }) + + return resultOne.rows; + + } + + static async get(playlistId) { + + const queryT = `SELECT * FROM playlists WHERE id=${playlistId}`; + await db.query(queryT); + + const queryT2 = `SELECT * from playlists_songs INNER JOIN songs ON playlists_songs.song_id = songs.id WHERE playlist_id=${playlistId}` + + const resultTwo = await db.query(queryT2); + + return resultTwo.rows; + } + + static async getAll() { + + const { rows } = await db.query('SELECT * FROM playlists'); + + return rows; + + } +} \ No newline at end of file diff --git a/models/song.js b/models/song.js new file mode 100644 index 00000000..50af23d3 --- /dev/null +++ b/models/song.js @@ -0,0 +1,46 @@ +const db = require('../util/database.js'); + +module.exports = class Song { + + constructor(title, album, link, art, artistId) { + this.title = title; + this.album = album; + this.link = link; + this.art = art; + this.artistId = artistId; + + } + + async save() { + const queryT = `INSERT INTO songs(title, album, preview_link, artwork, artist_id) VALUES ($1, $2, $3, $4, $5) RETURNING *`; + const queryV = [this.title, this.album, this.link, this.art, this.artistId] + + const { rows } = await db.query(queryT, queryV); + console.log(rows); + + } + + static async getAll(artistId) { + + const queryT = `SELECT * FROM songs WHERE artist_id = ${artistId} ORDER BY id` + const { rows } = await db.query(queryT); + + return rows; + } + + static async edit(title, album, link, art, artistId, songId) { + + const queryT = `UPDATE songs SET title = '${title}', album = '${album}', preview_link = '${link}', artwork = '${art}', artist_id = '${artistId}' WHERE id=${songId} RETURNING *` + + const { rows } = await db.query(queryT); + + return rows; + } + + static async delete(songId) { + const queryT = `DELETE from songs WHERE id=${songId}` + const { rows } = await db.query(queryT); + + return rows; + } +} \ No newline at end of file diff --git a/models/user.js b/models/user.js new file mode 100644 index 00000000..157b311b --- /dev/null +++ b/models/user.js @@ -0,0 +1,9 @@ +const db = require('../util/database.js'); + +module.exports = class User { + + constructor(email, password) { + this.email = email; + this.password = password; + } +} \ No newline at end of file diff --git a/package.json b/package.json index 544d004b..2b9df89f 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,31 @@ { - "name": "tunr_express", - "version": "1.0.0", - "description": "We're going to be building Tunr, the worlds #1 music catalog / player (those Spotify haters can't keep up with us).", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "https://git.generalassemb.ly/ga-wdi-exercises/tunr_sinatra.git" - }, - "author": "", - "license": "ISC", - "dependencies": { - "express": "^4.16.3", - "express-react-views": "^0.10.5", - "method-override": "^3.0.0", - "pg": "^7.4.3", - "react": "^16.5.2", - "react-dom": "^16.5.2" - } + "name": "tunr_express", + "version": "1.0.0", + "description": "We're going to be building Tunr, the worlds #1 music catalog / player (those Spotify haters can't keep up with us).", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "nodemon ./routes/app.js" + }, + "repository": { + "type": "git", + "url": "https://git.generalassemb.ly/ga-wdi-exercises/tunr_sinatra.git" + }, + "author": "", + "license": "ISC", + "dependencies": { + "cookie-parser": "^1.4.5", + "express": "^4.16.3", + "express-promise-router": "^3.0.3", + "express-react-views": "^0.10.5", + "express-session": "^1.17.1", + "js-sha256": "^0.9.0", + "method-override": "^3.0.0", + "pg": "^7.4.3", + "react": "^16.5.2", + "react-dom": "^16.5.2" + }, + "devDependencies": { + "nodemon": "^2.0.3" + } } diff --git a/public/css/forms.css b/public/css/forms.css new file mode 100644 index 00000000..7c0a5ba3 --- /dev/null +++ b/public/css/forms.css @@ -0,0 +1,267 @@ +.form__wrapper { + width: 80%; + min-height: 50vh; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.add-form { + overflow-y: scroll; + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + height: 100%; + width: 90%; + color: #49494b; +} + +.add-form__header { + margin: 12px 0; + padding: 12px 0; + font-size: 1.6rem; +} + +.add-form input { + margin: 12px 0; + padding: 4px 0; + max-width: 900px; + width: 80%; + height: 35px; + color: #49494b; + font-size: 1rem; + border: 1px dotted #49494b; + text-align: center; + outline: none; + background-color: rgba(255, 255, 255, 0.4); +} + +.add-form__song-container { + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + width: 90%; + padding: 20px 0; + margin: 12px 0; + border-bottom: 1px dotted #49494b; + box-sizing: border-box; +} + +.add-form button { + cursor: pointer; + border-radius: 0px; + text-decoration: none; + margin: 12px 12px; + padding: 12px 18px; + font-size: 12px; + line-height: 19px; + min-width: 150px; + max-width: 200px; + width: 3 s0%; + text-transform: uppercase; + font-family: 'IBM Plex Sans', sans-serif; + font-weight: 400; + letter-spacing: 3px; + border: solid 2px #49494b; + background: transparent; + color: #49494b; + -webkit-transition: all .1s linear; + -moz-transition: all .1s linear; + -ms-transition: all .1s linear; + -o-transition: all .1s linear; + transition: all .1s linear; + outline: none; + box-sizing: border-box; +} + +.add-form button:hover { + background-color: #49494b; + color: white; + border-radius: 30px; + transition: all .4s ease-out; + -webkit-transition: all .4s ease-out; + -moz-transition: all .4s ease-out; + -ms-transition: all .4s ease-out; + -o-transition: all .4s ease-out; +} + +.add-form button:active { + background-color: #49494b; + color: lightgrey; + transition: all .1s linear; + -webkit-transition: all .1s linear; + -moz-transition: all .1s linear; + -ms-transition: all .1s linear; + -o-transition: all .1s linear; +} + +.add-form__submit-btn { + width: 80% !important; +} + +.add-form h2, +.add-form h3, +.add-form h4 { + width: 100%; + padding: 10px 0; + text-align: center; + font-size: 1.6rem; + font-weight: 600; + letter-spacing: 0.1rem; + color: #49494b; + /* margin: 12px 0;*/ +} + +.add-form>select { + width: 80%; + max-width: 900px; + height: 20%; + -webkit-appearance: none; + padding: 8px 16px; + margin: 12px 0; + color: #49494b; + background-color: rgba(255, 255, 255, 0.4); + outline: none; +} + +.add-playlist-btn { + visibility: hidden; +} + +/*edit*/ + +.edit-form { + overflow-y: scroll; + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + height: 100%; + width: 90%; + color: #49494b; +} + +.edit-form__header { + margin: 12px 0; + padding: 12px 0; + font-size: 1.6rem; +} + +.edit-form input { + margin: 12px 0; + width: 80%; + height: 35px; + color: #49494b; + font-size: 1rem; + border: 1px dotted #49494b; + text-align: center; + outline: none; +} + +.edit-form button { + cursor: pointer; + border-radius: 0px; + text-decoration: none; + margin: 12px 12px; + padding: 12px 18px; + font-size: 12px; + line-height: 19px; + min-width: 100px; + width: 30%; + max-width: 200px; + text-transform: uppercase; + font-family: 'IBM Plex Sans', sans-serif; + font-weight: 400; + letter-spacing: 3px; + border: solid 2px #49494b; + background: transparent; + color: #49494b; + -webkit-transition: all .1s linear; + -moz-transition: all .1s linear; + -ms-transition: all .1s linear; + -o-transition: all .1s linear; + transition: all .1s linear; + outline: none; + box-sizing: border-box; +} + +.edit-form button:hover { + background-color: #49494b; + color: white; + border-radius: 30px; + transition: all .4s ease-out; + -webkit-transition: all .4s ease-out; + -moz-transition: all .4s ease-out; + -ms-transition: all .4s ease-out; + -o-transition: all .4s ease-out; +} + +.edit-form button:active { + background-color: #49494b; + color: lightgrey; + transition: all .1s linear; + -webkit-transition: all .1s linear; + -moz-transition: all .1s linear; + -ms-transition: all .1s linear; + -o-transition: all .1s linear; +} + +.edit-form__submit-btn { + width: 80% !important; +} + +.edit-form h2, +.edit-form h3, +.edit-form h4, +.delete__header, +.not-found__header, + { + width: 100%; + padding: 10px 0; + text-align: center; + font-size: 1.6rem; + font-weight: 600; + letter-spacing: 0.1rem; + color: #49494b; + /* margin: 12px 0;*/ +} + +.edit-form__song-container { + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + width: 90%; + padding: 20px 0; + margin: 12px 0; + border-bottom: 1px dotted #49494b; + box-sizing: border-box; +} + +.edit-form>select { + width: 40%; + height: 20%; + -webkit-appearance: none; + padding: 8px 16px; + margin: 12px 0; + color: #49494b; +} + +/**/ + +.form__btn-wrapper { + width: 100%; + display: flex; + justify-content: center; +} + +/**/ + +.logout-form { + overflow-y: hidden; + height: 100%; +} \ No newline at end of file diff --git a/public/css/main.css b/public/css/main.css new file mode 100644 index 00000000..d0a9d9d5 --- /dev/null +++ b/public/css/main.css @@ -0,0 +1,638 @@ +* { + margin: 0; + padding: 0; + z-index: 1; + /*font-family: 'Roboto Slab', serif;*/ + font-family: 'IBM Plex Sans', sans-serif; +} + +html { + width: 100%; + height: 100%; +} + +body { + display: flex; + width: 100%; + height: 100%; + flex-direction: column; + align-items: center; + z-index: 2; +} + +.overlay { + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + background: linear-gradient(to right, + #aa4b6b 0%, + #6b6b83 50%, + #6b6b83 50%, + #3b8d99 100%); + opacity: 0.22; + z-index: 0; +} + +.header { + width: 100%; + height: 110px; + display: flex; + justify-content: center; + z-index: 5; +} + +.header>img { + width: 240px; + opacity: 0.9; +} + +main { + display: flex; + flex-direction: column; + width: 100%; + height: auto; + justify-content: space-around; + align-items: center; + z-index: 3; +} + +.banner { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 40%; + min-height: 600px; + max-height: 1000px; + background-image: url('https://images.unsplash.com/photo-1518481852452-9415b262eba4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80'); + background-repeat: no-repeat; + background-size: cover; +} + +.landing-page__nav { + width: 100%; + height: 77vh; + min-height: 600px; + display: flex; + flex-wrap: wrap; + flex-direction: row; + justify-content: center; + align-items: center; + background-color: rgba(255, 255, 255, 0.4); +} + +.not-found__nav { + height: 40vh; +} + +.landing-page-link { + box-sizing: border-box; + padding: 100px 0; + width: 80%; + min-width: 320px; + text-decoration: none; + text-align: center; + font-weight: 600; + font-size: 1.6rem; + letter-spacing: 1.2rem; + color: rgba(1, 1, 1, 0.8); + background-color: lightgrey; + opacity: 0.5; +} + +.landing-page-link:hover { + color: white; + transition: all .2s linear; +} + +.nav { + width: 100%; + height: auto; + display: flex; + flex-wrap: wrap; + flex-direction: row; + justify-content: space-around; + align-items: center; + background-color: rgba(1, 1, 1, 0.2); +} + +.nav__link { + width: auto; + min-width: 25%; + height: 80px; + text-decoration: none; + text-align: center; + margin: 0 5px; + font-weight: 500; + font-size: 1rem; + letter-spacing: 0.9rem; + color: rgba(1, 1, 1, 0.9); + line-height: 200%; + text-transform: uppercase; + box-sizing: border-box; + opacity: 0.8; + display: flex; + align-items: center; + justify-content: center; +} + +.nav__link>p { + width: 100%; +} + + +.nav__link:hover { + color: white; + transition: all .2s linear; +} + +/*login-register*/ + +.login-register__wrapper { + padding: 18px 0; + display: flex; + justify-content: space-around; + align-items: center; + width: 100%; + max-width: 600px; + height: auto; + /* background-color: rgba(1, 1, 1, 0.2);*/ +} + +.login-register__wrapper.home { + flex-direction: column; +} + +.login-register__wrapper>.user__wrapper { + width: 100%; + height: auto; + padding: 12px 0; + display: flex; + justify-content: center; +} + +.user__wrapper>p { + width: 100%; + font-size: 0.8rem; + text-align: center; + color: #49494b +} + +.login-register__wrapper>a { + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + height: 100%; + padding: 2%; + width: 30%; + text-decoration: none; + color: rgba(1, 1, 1, 0.6); + letter-spacing: 0.1rem; + font-weight: 500; + /*background-color: rgba(1, 1, 1, 0.1);*/ + text-align: center; + font-size: 0.8rem; + text-transform: uppercase; + border: 2px solid lightgrey; +} + +.login-register__wrapper>a.favourites-link { + height: 50%; + margin: 10px 0; + width: 150px; +} + +.login-register__wrapper>a:hover { + color: darkred; +} + +.login-register__wrapper>a:active { + color: red; + letter-spacing: 0.12rem; +} + +/*home*/ + +.featured-artist { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + flex-grow: 0; + z-index: 2; +} + +.featured-artist__header { + width: 100%; + text-align: center; + font-size: 1.2rem; + letter-spacing: 0.8rem; + font-weight: 400; + margin: 28px 0; + color: rgba(1, 1, 1, 0.6); + text-transform: uppercase; + text-align: center; + text-indent: 0.2rem; +} + +.featured-artist.login-register-page { + flex-grow: 0; +} + +.featured-artist__header.login-register-page { + margin-top: 0; +} + +.featured-artist__name { + color: rgba(1, 1, 1, 0.6); + width: 100%; + text-align: center; + font-size: 0.9rem; + letter-spacing: 0.2rem; + text-decoration: none; + cursor: pointer; + pointer-events: all; +} + +.featured-artist__name:hover { + color: rgba(10, 99, 200, 0.6); + transition: all .2s linear; +} + +.featured-artist__img-wrapper { + width: 100%; + height: auto; + display: flex; + justify-content: center; +} + +.featured-artist__img { + margin: 28px 0; + width: 80%; + max-width: 800px; + height: auto; +} + +/*show all artists*/ + +.single-artist__container { + width: 100%; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + box-sizing: border-box; +} + +.single-artist__container:first-of-type, +.single-artist__container.single-display, +.single-song__container:first-of-type, +.single-song__container.single-display { + margin-top: 58px; +} + +.single-artist__img-container { + display: flex; + width: 40%; + height: auto; + justify-content: center; + align-items: center; +} + +.single-artist__img { + width: 300px; + height: 300px; + object-fit: cover; +} + +.single-artist__img-container.single-display { + width: 80%; + max-width: 600px; +} + +.single-artist__img.single-display { + width: 100%; + height: 100%; + object-fit: cover; +} + +.single-artist__name, +.single-artist__nationality { + color: rgba(1, 1, 1, 0.6); + width: 100%; + text-align: center; + font-size: 1.1rem; + letter-spacing: 0.3rem; + text-decoration: none; + box-sizing: border-box; +} + +.single-artist__name { + margin-top: 2%; + margin-bottom: 4px; + font-weight: 700; +} + +.single-artist__name:hover { + opacity: 0.6; +} + +.single-artist__nationality { + margin-top: 4px; + margin-bottom: 4%; +} + +.artist__edit-delete-links, +.song__edit-delete-links, +.playlist__edit-delete-links { + display: flex; + justify-content: space-around; + align-items: center; + width: 100%; + max-width: 600px; + height: 4rem; + margin-bottom: 2%; + /* background-color: rgba(1, 1, 1, 0.2);*/ +} + +.artist__edit-link, +.artist__delete-link, +.song__delete-link, +.song__edit-link, +.playlist__edit-link, +.playlist__delete-link { + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + height: 100%; + padding: 2%; + width: 30%; + text-decoration: none; + color: rgba(1, 1, 1, 0.6); + letter-spacing: 0.1rem; + font-weight: 500; + /*background-color: rgba(1, 1, 1, 0.1);*/ + text-align: center; + font-size: 0.8rem; + text-transform: uppercase; + border: 2px solid lightgrey; +} + +.artist__edit-link:hover, +.artist__delete-link:hover, +.song__edit-link:hover, +.song__delete-link:hover, +.playlist__edit-link:hover, +.playlist__delete-link:hover { + color: darkred; +} + +.artist__edit-link:active, +.artist__delete-link:active, +.song__edit-link:active, +.song__delete-link:active, +.playlist__edit-link:active, +.playlist__delete-link:active { + color: red; + letter-spacing: 0.12rem; +} + +/*songs*/ + +.single-song__container { + width: 100%; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + box-sizing: border-box; +} + +.single-song__container:first-of-type { + margin-top: 6%; +} + +.single-song__img-container { + display: flex; + width: 40%; + height: auto; + justify-content: center; + align-items: center; +} + +.single-song__img { + background-color: lightgrey; + width: 300px; + height: 300px; + object-fit: cover; +} + +.single-song__title, +.single-song__album { + color: rgba(1, 1, 1, 0.6); + width: 100%; + text-align: center; + font-size: 1.1rem; + letter-spacing: 0.3rem; + text-decoration: none; + box-sizing: border-box; +} + +.single-song__title { + margin-top: 1%; + margin-bottom: 4px; + font-weight: 700; +} + +.single-song__album { + margin-top: 4px; + margin-bottom: 4%; +} + +.single-artist__songs-link { + color: rgba(1, 1, 1, 0.6); + text-decoration: none; + width: auto; + margin-bottom: 3%; + display: flex; + justify-content: center; + align-items: center; + text-transform: uppercase; + text-align: center; + letter-spacing: 0.3rem; + font-size: 1.2rem; + padding: 10px 4px; + border-top: 1px dashed lightgrey; + border-bottom: 1px dashed lightgrey; +} + +.single-artist__songs-link:hover { + background-color: lightgrey; + color: white; +} + +.single-playlist__container.single-display { + width: 100%; + height: auto; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.single-song__container>a { + color: rgba(1, 1, 1, 0.6); + text-decoration: none; + width: auto; + margin-bottom: 3%; + display: flex; + justify-content: center; + align-items: center; + text-transform: uppercase; + text-align: center; + letter-spacing: 0.3rem; + font-size: 1.2rem; + padding: 10px 4px; + border-top: 1px dashed lightgrey; + border-bottom: 1px dashed lightgrey; +} + +.single-song__container>a:hover { + background-color: lightgrey; + color: white; +} + +.single-song__container button { + font-size: 0.6rem; + cursor: pointer; + border-radius: 0px; + text-decoration: none; + margin: 0px 12px 24px 0; + padding: 12px 18px; + line-height: 19px; + min-width: 150px; + max-width: 200px; + width: 3 s0%; + text-transform: uppercase; + font-family: 'IBM Plex Sans', sans-serif; + font-weight: 400; + letter-spacing: 3px; + border: solid 2px #49494b; + background: transparent; + color: #49494b; + -webkit-transition: all .1s linear; + -moz-transition: all .1s linear; + -ms-transition: all .1s linear; + -o-transition: all .1s linear; + transition: all .1s linear; + outline: none; + box-sizing: border-box; +} + +.single-song__container button:hover { + background-color: #49494b; + color: white; + border-radius: 30px; + transition: all .4s ease-out; + -webkit-transition: all .4s ease-out; + -moz-transition: all .4s ease-out; + -ms-transition: all .4s ease-out; + -o-transition: all .4s ease-out; +} + +.single-song__container button:active { + background-color: #49494b; + color: lightgrey; + transition: all .1s linear; + -webkit-transition: all .1s linear; + -moz-transition: all .1s linear; + -ms-transition: all .1s linear; + -o-transition: all .1s linear; +} + +/*playlists*/ + +.single-playlist__container { + width: 100%; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + box-sizing: border-box; +} + +.single-playlist__container>a { + color: rgba(1, 1, 1, 0.6); + text-decoration: none; + width: auto; + margin-bottom: 3%; + display: flex; + justify-content: center; + align-items: center; + text-transform: uppercase; + text-align: center; + letter-spacing: 0.3rem; + font-size: 1.2rem; + padding: 10px 4px; + border-top: 1px dashed lightgrey; + border-bottom: 1px dashed lightgrey; +} + +.single-playlist__container>a:hover { + background-color: lightgrey; + color: white; +} + +.playlists-all__header { + width: 100%; + padding: 10px 0; + text-align: center; + font-size: 1.6rem; + font-weight: 600; + letter-spacing: 0.1rem; + color: #49494b; + /* margin: 12px 0;*/ + margin-bottom: 12px; +} + +/*favourites*/ +.favourites__header { + display: flex; + flex-direction: column; + width: 50%; + justify-content: space-around; + align-items: center; + padding: 12px 0; + margin-bottom: 12px; + border-bottom: 1px #49494b dotted; +} + +.favourites__header>h2 { + width: 100%; + padding: 10px 0; + text-align: center; + font-size: 1.6rem; + font-weight: 600; + letter-spacing: 0.1rem; + color: #49494b; + /* margin: 12px 0;*/ +} + +.favourites__header>h4 { + width: 100%; + padding: 10px 0; + text-align: center; + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.1rem; + color: #49494b; + /* margin: 12px 0;*/ +} \ No newline at end of file diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 00000000..4f5295d0 Binary files /dev/null and b/public/images/logo.png differ diff --git a/public/scripts/add-form.js b/public/scripts/add-form.js new file mode 100644 index 00000000..a6178be2 --- /dev/null +++ b/public/scripts/add-form.js @@ -0,0 +1,26 @@ +const submitBtn = document.querySelector('.add-form__submit-btn'); + +// let nameInput, nationalityInput, imageLinkInput; +// [nameInput, nationalityInput, imageLinkInput] = document.querySelectorAll('.edit-form>input'); + +submitBtn.addEventListener('click', (ev) => { + + invalidMsg = []; + + document.querySelectorAll('.add-form>input').forEach(input => { + if (!input.value) invalidMsg.push(input.name) + }) + + if (invalidMsg.length > 0) { + + ev.preventDefault() + + let messageEl = document.createElement('p'); + + messageEl.innerText = invalidMsg.reduce((msg, input) => { + return msg.concat(input, "\n"); + }, 'The following fields are empty:\n') + + document.querySelector('.add-form').append(messageEl); + } +}) \ No newline at end of file diff --git a/public/scripts/add-playlist-form.js b/public/scripts/add-playlist-form.js new file mode 100644 index 00000000..99c75499 --- /dev/null +++ b/public/scripts/add-playlist-form.js @@ -0,0 +1,50 @@ +const addSongBtn = document.querySelector('.add-form__add-song-btn'); +const deleteSongBtn = document.querySelector('.add-form__delete-song-btn'); +const addPlaylistBtn = document.querySelector('.add-form__submit-btn'); +const addPlaylistForm = document.querySelector('.add-form'); + + +const songSelect = document.getElementById('songs-select'); +const artistSelect = document.getElementById('artists-select'); + +const filterSongOptions = () => { + + const songOptions = document.querySelectorAll('.song-options'); + songOptions.forEach(option => { + option.getAttribute("artist") !== artistSelect.value ? + option.hidden = true : + option.hidden = false; + }) + + songSelect.value = ""; +} + +artistSelect.addEventListener('change', filterSongOptions); + +addSongBtn.addEventListener('click', () => { + + addPlaylistBtn.style.visibility = 'visible'; + + document.querySelector('.form__btn-wrapper').insertAdjacentHTML('afterend', + ` +
+
Artist:
+ +
Song:
+ +
+ ` + ) + +}) + +deleteSongBtn.addEventListener('click', () => { + + if (document.querySelectorAll('.add-form__song-container').length <= 1) + addPlaylistBtn.style.visibility = 'hidden'; + + const songInputs = document.querySelectorAll('.add-form__song-container'); + + addPlaylistForm.removeChild(songInputs[songInputs.length - 1]); + +}) \ No newline at end of file diff --git a/public/scripts/edit-form.js b/public/scripts/edit-form.js new file mode 100644 index 00000000..7deec8ad --- /dev/null +++ b/public/scripts/edit-form.js @@ -0,0 +1,26 @@ +const submitBtnTwo = document.querySelector('.edit-form__submit-btn'); + +// let nameInput, nationalityInput, imageLinkInput; +// [nameInput, nationalityInput, imageLinkInput] = document.querySelectorAll('.edit-form>input'); + +submitBtnTwo.addEventListener('click', (ev) => { + + invalidMsg = []; + + document.querySelectorAll('.edit-form>input').forEach(input => { + if (!input.value) invalidMsg.push(input.name) + }) + + if (invalidMsg.length > 0) { + + ev.preventDefault() + + let messageEl = document.createElement('p'); + + messageEl.innerText = invalidMsg.reduce((msg, input) => { + return msg.concat(input, "\n"); + }, 'The following fields are empty:\n') + + document.querySelector('.edit-form').append(messageEl); + } +}) \ No newline at end of file diff --git a/public/scripts/edit-playlist-form.js b/public/scripts/edit-playlist-form.js new file mode 100644 index 00000000..970d8b16 --- /dev/null +++ b/public/scripts/edit-playlist-form.js @@ -0,0 +1,50 @@ +const addSongBtn = document.querySelector('.edit-form__add-song-btn'); +const deleteSongBtn = document.querySelector('.edit-form__delete-song-btn'); +const editPlaylistBtn = document.querySelector('.edit-form__submit-btn'); +const editPlaylistForm = document.querySelector('.edit-form'); + + +const songSelect = document.getElementById('songs-select'); +const artistSelect = document.getElementById('artists-select'); + +const filterSongOptions = () => { + + const songOptions = document.querySelectorAll('.song-options'); + songOptions.forEach(option => { + option.getAttribute("artist") !== artistSelect.value ? + option.hidden = true : + option.hidden = false; + }) + + songSelect.value = ""; +} + +artistSelect.addEventListener('change', filterSongOptions); + +addSongBtn.addEventListener('click', () => { + + editPlaylistBtn.style.visibility = 'visible'; + + document.querySelector('.form__btn-wrapper').insertAdjacentHTML('afterend', + ` +
+
Artist:
+ +
Song:
+ +
+ ` + ) + +}) + +deleteSongBtn.addEventListener('click', () => { + + if (document.querySelectorAll('.edit-form__song-container').length <= 1) + editPlaylistBtn.style.visibility = 'hidden'; + + const songInputs = document.querySelectorAll('.edit-form__song-container'); + + editPlaylistForm.removeChild(songInputs[songInputs.length - 1]); + +}) \ No newline at end of file diff --git a/routes/app.js b/routes/app.js new file mode 100644 index 00000000..50725530 --- /dev/null +++ b/routes/app.js @@ -0,0 +1,137 @@ +console.log("starting up!!"); + +const express = require('express'); +const methodOverride = require('method-override'); +const pg = require('pg'); +const session = require('express-session'); +const cookieParser = require('cookie-parser'); + + +// Initialise postgres client + +/** + * =================================== + * Configurations and set up + * =================================== + */ + +// Init express app +const app = express(); + +app.use(express.json()); +app.use(express.urlencoded({ + extended: true +})); + +app.use(cookieParser()); + +app.use(methodOverride('_method')); + +// Set session +app.use(session({ + secret: 'tunr!secret', + resave: false, + saveUninitialized: false, + name: 'sid', + cookie: { + maxAge: 86400000, + sameSite: true + } +})) + + +// Set react-views to be the default view engine +const reactEngine = require('express-react-views').createEngine(); +const path = require('path'); + +app.set('views', path.join(__dirname, '..', '/views')); +app.set('view engine', 'jsx'); +app.engine('jsx', reactEngine); + +const artistsRoutes = require('./artists-routes.js'); +const playlistsRoutes = require('./playlists-routes.js'); +const authRoutes = require('./auth-routes.js'); +const favouritesRoutes = require('./favourites-routes.js'); +const authController = require('../controllers/auth-controller.js'); +const errorController = require('../controllers/404-controller.js'); + +const db = require('../util/database.js'); + +app.use(express.static(path.join(__dirname, '../public/'))); + +app.use('/', async (req, res, next) => { + const queryT = `SELECT * FROM artists WHERE id=3` + const { rows } = await db.query(queryT); + req.featuredArtist = rows[0]; + next(); +}) + +app.use('/auth', authRoutes); + +app.use('/', async (req, res, next) => { + + if (req.session.userId) { + + req.currentUser = await authController.getUserInfo(req.session.userId); + + } + + next(); +}) + +app.get('/', async (req, res) => { + + //redirect to homepage with auth routes if user is not logged in + + if (req.session.userId) { + + req.currentUser = await authController.getUserInfo(req.session.userId); + + res.cookie('userId', req.session.userId); + + await authController.visitsCookieCounter(req, res); + + res.render('home', { + 'singleArtist': req.featuredArtist, + 'currentUser': req.currentUser + }); + + } else { + + res.redirect('/auth'); + + } + +}); + +app.use('/favourites', favouritesRoutes); + +app.use('/artists', artistsRoutes); + +app.use('/playlists', playlistsRoutes); + +app.use(errorController.get404Page); + + +/** + * =================================== + * Listen to requests on port 3000 + * =================================== + */ +const server = app.listen(3000, () => console.log('~~~ Tuning in to the waves of port 3000 ~~~')); + +let onClose = function() { + + console.log("\nclosing"); + + server.close(() => { + + db.poolEnd(); + + console.log('Process terminated'); + + }) +}; + +process.on('SIGTERM', onClose); +process.on('SIGINT', onClose); \ No newline at end of file diff --git a/routes/artists-routes.js b/routes/artists-routes.js new file mode 100644 index 00000000..fb2aa489 --- /dev/null +++ b/routes/artists-routes.js @@ -0,0 +1,54 @@ +const express = require('express'); +const Router = require('express-promise-router'); +const router = new Router(); +const path = require('path'); +const db = require('../util/database.js'); + +const artistsController = require('../controllers/artists-controller'); +const songsController = require('../controllers/songs-controller'); +const errorController = require('../controllers/404-controller.js'); + +const songsRoutes = require('./songs-routes.js'); + +router.use('/', express.static(path.join(__dirname, '..', '/public/'))); +router.use('/:id', express.static(path.join(__dirname, '..', '/public/'))); + + +//Middleware to store artist properties in request before moving on to songsRoutes. Necessary for queries made in songs-controller as artist id in url will not be exposed there. + +router.use('/:id/songs', async (req, res, next) => { + + const { id } = req.params; + + const queryT = `SELECT * FROM artists WHERE id=${id}` + + try { + const { rows } = await db.query(queryT); + req.artist = rows[0] + + } catch (e) { + res.status(404).render('404') + } + + next(); +}) + +router.use('/:id/songs', songsRoutes); + +router.get('/:id/edit', artistsController.getEditArtistById); + +router.get('/:id/delete', artistsController.deleteArtistById); + +router.get('/new', artistsController.getAddArtist); + +router.get('/:id', artistsController.getArtistById); + +router.put('/:id', artistsController.putArtistById) + +router.post('/', artistsController.postAddArtist); + +router.get('/', artistsController.getAllArtists); + +router.use(errorController.get404Page); + +module.exports = router; \ No newline at end of file diff --git a/routes/auth-routes.js b/routes/auth-routes.js new file mode 100644 index 00000000..70f40328 --- /dev/null +++ b/routes/auth-routes.js @@ -0,0 +1,26 @@ +const express = require('express'); +const Router = require('express-promise-router'); +const router = new Router(); +const path = require('path'); +const session = require('express-session'); +const cookieParser = require('cookie-parser'); + +router.use(cookieParser()); + +router.use('/:route', express.static(path.join(__dirname, '..', '/public/'))); + +const authController = require('../controllers/auth-controller.js'); + +router.get('/login', authController.getLogin); + +router.get('/register', authController.getRegister); + +router.post('/login', authController.postLogin); + +router.post('/register', authController.postRegister); + +router.post('/logout', authController.postLogout); + +router.get('/', authController.getLoginRegister); + +module.exports = router; \ No newline at end of file diff --git a/routes/favourites-routes.js b/routes/favourites-routes.js new file mode 100644 index 00000000..cd3da150 --- /dev/null +++ b/routes/favourites-routes.js @@ -0,0 +1,23 @@ +const Router = require('express-promise-router'); +const db = require('../util/database.js'); + +const express = require('express'); +const path = require('path'); + +const router = new Router(); + +const favouritesController = require('../controllers/favourites-controller.js'); +const errorController = require('../controllers/404-controller.js'); + +router.use('/', express.static(path.join(__dirname, '..', '/public/'))); +router.use('/:id', express.static(path.join(__dirname, '..', '/public/'))); + +router.post('/new', favouritesController.postFavourites); + +router.delete('/delete', favouritesController.deleteFavourites); + +router.get('/', favouritesController.getAllFavourites); + +router.use(errorController.get404Page); + +module.exports = router; \ No newline at end of file diff --git a/routes/playlists-routes.js b/routes/playlists-routes.js new file mode 100644 index 00000000..a2d4ba2b --- /dev/null +++ b/routes/playlists-routes.js @@ -0,0 +1,29 @@ +const express = require('express'); +const Router = require('express-promise-router'); +const router = new Router(); +const path = require('path'); +const db = require('../util/database.js'); + +const playlistsController = require('../controllers/playlists-controller'); +const errorController = require('../controllers/404-controller.js'); + +router.use('/', express.static(path.join(__dirname, '..', '/public/'))); +router.use('/:id', express.static(path.join(__dirname, '..', '/public/'))); + +router.get('/:id/edit', playlistsController.getEditPlaylistById); + +router.get('/:id/delete', playlistsController.deletePlaylistById); + +router.get('/new', playlistsController.getAddPlaylist); + +router.get('/:id', playlistsController.getPlaylistById); + +router.put('/:id', playlistsController.putPlaylistById) + +router.post('/', playlistsController.postAddPlaylist); + +router.get('/', playlistsController.getAllPlaylists); + +router.use(errorController.get404Page); + +module.exports = router; \ No newline at end of file diff --git a/routes/songs-routes.js b/routes/songs-routes.js new file mode 100644 index 00000000..9755c727 --- /dev/null +++ b/routes/songs-routes.js @@ -0,0 +1,40 @@ +const Router = require('express-promise-router'); +const db = require('../util/database.js'); + +const express = require('express'); +const path = require('path'); + +const router = new Router(); + +const songsController = require('../controllers/songs-controller'); +const errorController = require('../controllers/404-controller.js'); + +router.use('/', express.static(path.join(__dirname, '..', '/public/'))); +router.use('/:id', express.static(path.join(__dirname, '..', '/public/'))); + +router.get('/add', songsController.getAddSongToArtist); + +router.use('/:id', async (req, res, next) => { + const { id } = req.params; + const queryT = `SELECT * FROM songs WHERE artist_id=${req.artist.id} ORDER BY id` + const { rows } = await db.query(queryT); + req.song = rows[id]; + !req.song ? res.status(404).render('404') : req.song.position = id; + next(); +}) + +router.get('/:id/edit', songsController.getEditArtistSongById); + +router.get('/:id/delete', songsController.deleteArtistSongById); + +router.get('/:id', songsController.getArtistSongById); + +router.put('/:id', songsController.putArtistSongById) + +router.post('/', songsController.postAddSongToArtist); + +router.get('/', songsController.getAllSongsOfArtist); + +router.use(errorController.get404Page); + +module.exports = router; \ No newline at end of file diff --git a/util/database.js b/util/database.js new file mode 100644 index 00000000..a379b3a1 --- /dev/null +++ b/util/database.js @@ -0,0 +1,41 @@ +const { Pool } = require('pg'); + +const configs = { + user: 'zachariah', + host: '127.0.0.1', + database: 'tunr_db', + password: 'zachariah', + port: 5432, +}; + +const pool = new Pool(configs); + +pool.on('error', function(err) { + console.log('idle client error', err.message, err.stack); +}); + + +module.exports = { + poolEnd: async () => { + await pool.end(() => console.log('\nShut down db connection pool')); + }, + + query: async (queryText, queryValues) => { + + try { + + const client = await pool.connect(); + console.log('connected!'); + + const res = await client.query(queryText, queryValues); + + client.release(); + console.log('client released!'); + + return res; + + } catch (e) { + console.log(`Error\n` + e.message, e.stack); + } + } +} \ No newline at end of file diff --git a/util/get-date.js b/util/get-date.js new file mode 100644 index 00000000..00b8c8a3 --- /dev/null +++ b/util/get-date.js @@ -0,0 +1,7 @@ +module.exports.getTimeStamp = () => { + return new Date(Date.now() - new Date() + .getTimezoneOffset() * 60000) + .toISOString() + .slice(0, 19) + .replace('T', ' '); +}; \ No newline at end of file diff --git a/views/404.jsx b/views/404.jsx new file mode 100644 index 00000000..96ea5a01 --- /dev/null +++ b/views/404.jsx @@ -0,0 +1,30 @@ +const React = require("react"); + +import Head from './page-components/head-component'; +import Header from './page-components/header-component'; + +class NotFoundPage extends React.Component { + + render() { + + return ( + + +
+ +
+
+
+
+
+ PAGE DOESN'T EXIST, LET ME TAKE YOU BACK HOME +
+
+
+ + + ); + } +} + +module.exports = NotFoundPage; \ No newline at end of file diff --git a/views/artists/add-artist.jsx b/views/artists/add-artist.jsx new file mode 100644 index 00000000..00a5ffb9 --- /dev/null +++ b/views/artists/add-artist.jsx @@ -0,0 +1,38 @@ +const React = require("react"); + +import Head from '../page-components/head-component'; +import Header from '../page-components/header-component'; + +class AddArtistForm extends React.Component { + + render() { + + return ( + +
+ +