diff --git a/api/actor.php b/api/actor.php new file mode 100644 index 0000000..36a0d3c --- /dev/null +++ b/api/actor.php @@ -0,0 +1,5 @@ +handleAction(); diff --git a/api/src/handlers/Actor.php b/api/src/handlers/Actor.php new file mode 100644 index 0000000..db33848 --- /dev/null +++ b/api/src/handlers/Actor.php @@ -0,0 +1,73 @@ +databaseAdds(); + $this->databaseRequests(); + } + + function databaseAdds() { + $this->addActionHandler("createActor", function () { + // skip tag create if already existing + $actorname = $_POST["actorname"]; + + $query = "INSERT IGNORE INTO actors (name) VALUES ('$actorname')"; + + if ($this->conn->query($query) === TRUE) { + $this->commitMessage('{"result":"success"}'); + } else { + $this->commitMessage('{"result":"' . $this->conn->error . '"}'); + } + }); + + $this->addActionHandler("addActorToVideo", function () { + // skip tag create if already existing + $actorid = $_POST["actorid"]; + $videoid = $_POST["videoid"]; + + $query = "INSERT IGNORE INTO actors_videos (actor_id, video_id) VALUES ($actorid,$videoid)"; + + if ($this->conn->query($query) === TRUE) { + $this->commitMessage('{"result":"success"}'); + } else { + $this->commitMessage('{"result":"' . $this->conn->error . '"}'); + } + }); + } + + function databaseRequests() { + $this->addActionHandler("getAllActors", function () { + // query the actors corresponding to video + $query = "SELECT * FROM actors"; + $result = $this->conn->query($query); + $this->commitMessage(json_encode(mysqli_fetch_all($result, MYSQLI_ASSOC))); + }); + + $this->addActionHandler("getActorsOfVideo", function () { + // query the actors corresponding to video + $video_id = $_POST["videoid"]; + + $query = "SELECT actor_id, name, thumbnail FROM actors_videos + JOIN actors a on actors_videos.actor_id = a.id + WHERE actors_videos.video_id=$video_id"; + $result = $this->conn->query($query); + $this->commitMessage(json_encode(mysqli_fetch_all($result, MYSQLI_ASSOC))); + }); + + $this->addActionHandler("getActorInfo", function (){ + $actorid = $_POST["actorid"]; + + $query = "SELECT movie_id, movie_name FROM actors_videos + JOIN videos v on v.movie_id = actors_videos.video_id + WHERE actors_videos.actor_id=$actorid"; + $result = $this->conn->query($query); + + $reply = array("videos" => mysqli_fetch_all($result, MYSQLI_ASSOC)); + + $this->commitMessage(json_encode($reply)); + }); + } +} diff --git a/api/src/handlers/RequestBase.php b/api/src/handlers/RequestBase.php index 92542a0..738cd8d 100644 --- a/api/src/handlers/RequestBase.php +++ b/api/src/handlers/RequestBase.php @@ -30,7 +30,7 @@ abstract class RequestBase { // call the right handler $this->actions[$action](); } else { - echo('{data:"error"}'); + $this->commitMessage('{"data": "error"}'); } } diff --git a/api/src/handlers/Video.php b/api/src/handlers/Video.php index fdee534..27c173f 100755 --- a/api/src/handlers/Video.php +++ b/api/src/handlers/Video.php @@ -98,6 +98,7 @@ class Video extends RequestBase { $this->addActionHandler("loadVideo", function () { $video_id = $_POST['movieid']; + // todo join with actor db and add actors of movieid $query = " SELECT movie_name,movie_id,movie_url,thumbnail,poster,likes,quality,length FROM videos WHERE movie_id=$video_id"; @@ -145,6 +146,13 @@ class Video extends RequestBase { array_push($arr['suggesttag'], $r); } + // query the actors corresponding to video + $query = "SELECT actor_id, name, thumbnail FROM actors_videos + JOIN actors a on actors_videos.actor_id = a.id + WHERE actors_videos.video_id=$video_id"; + $result = $this->conn->query($query); + $arr['actors'] = mysqli_fetch_all($result, MYSQLI_ASSOC); + $this->commitMessage(json_encode($arr)); }); diff --git a/database.sql b/database.sql index 739b058..cb5e642 100644 --- a/database.sql +++ b/database.sql @@ -1,3 +1,22 @@ +create table if not exists actors +( + id int auto_increment + primary key, + name varchar(50) null, + thumbnail mediumblob null +) + comment 'informations about different actors'; + +create table if not exists settings +( + video_path varchar(255) null, + episode_path varchar(255) null, + password varchar(32) null, + mediacenter_name varchar(32) null, + TMDB_grabbing tinyint null, + DarkMode tinyint default 0 null +); + create table if not exists tags ( tag_id int auto_increment @@ -12,13 +31,29 @@ create table if not exists videos movie_name varchar(200) null, movie_url varchar(250) null, thumbnail mediumblob null, - poster mediumblob null, likes int default 0 null, + create_date datetime default CURRENT_TIMESTAMP null, quality int null, length int null comment 'in seconds', - create_date datetime default CURRENT_TIMESTAMP null + poster mediumblob null ); +create table if not exists actors_videos +( + actor_id int null, + video_id int null, + constraint actors_videos_actors_id_fk + foreign key (actor_id) references actors (id), + constraint actors_videos_videos_movie_id_fk + foreign key (video_id) references videos (movie_id) +); + +create index actors_videos_actor_id_index + on actors_videos (actor_id); + +create index actors_videos_video_id_index + on actors_videos (video_id); + create table if not exists video_tags ( tag_id int null, @@ -30,15 +65,6 @@ create table if not exists video_tags on delete cascade ); -create table if not exists settings -( - video_path varchar(255) null, - episode_path varchar(255) null, - password varchar(32) default '-1' null, - mediacenter_name varchar(32) default 'OpenMediaCenter' null, - TMDB_grabbing tinyint null, - DarkMode tinyint default 0 null -); INSERT IGNORE INTO tags (tag_id, tag_name) VALUES (2, 'fullhd'); diff --git a/src/App.js b/src/App.js index 3409221..78a62c9 100644 --- a/src/App.js +++ b/src/App.js @@ -28,6 +28,9 @@ class App extends React.Component { // bind this to the method for being able to call methods such as this.setstate this.changeRootElement = this.changeRootElement.bind(this); this.returnToLastElement = this.returnToLastElement.bind(this); + + // set the main navigation viewbinding to the singleton + GlobalInfos.setViewBinding(this.constructViewBinding()); } componentDidMount() { @@ -68,16 +71,16 @@ class App extends React.Component { MainBody() { let page; if (this.state.page === 'default') { - page = ; + page = ; this.mypage = page; } else if (this.state.page === 'random') { - page = ; + page = ; this.mypage = page; } else if (this.state.page === 'settings') { page = ; this.mypage = page; } else if (this.state.page === 'categories') { - page = ; + page = ; this.mypage = page; } else if (this.state.page === 'video') { // show videoelement if neccessary diff --git a/src/GlobalInfos.js b/src/GlobalInfos.js index 0ef18ff..d8c1094 100644 --- a/src/GlobalInfos.js +++ b/src/GlobalInfos.js @@ -7,6 +7,7 @@ import lighttheme from './AppLightTheme.module.css'; */ class StaticInfos { #darktheme = true; + #viewbinding = () => {console.warn("Viewbinding not set now!")} /** * check if the current theme is the dark theme @@ -31,6 +32,22 @@ class StaticInfos { getThemeStyle() { return this.isDarkTheme() ? darktheme : lighttheme; } + + /** + * set the global Viewbinding for the main Navigation + * @param cb + */ + setViewBinding(cb){ + this.#viewbinding = cb; + } + + /** + * return the Viewbinding for main navigation + * @returns {StaticInfos.viewbinding} + */ + getViewBinding(){ + return this.#viewbinding; + } } const GlobalInfos = new StaticInfos(); diff --git a/src/elements/ActorTile/ActorTile.js b/src/elements/ActorTile/ActorTile.js new file mode 100644 index 0000000..383ad35 --- /dev/null +++ b/src/elements/ActorTile/ActorTile.js @@ -0,0 +1,43 @@ +import style from './ActorTile.module.css'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faUser} from '@fortawesome/free-solid-svg-icons'; +import React from 'react'; +import GlobalInfos from '../../GlobalInfos'; +import ActorPage from '../../pages/ActorPage/ActorPage'; + +class ActorTile extends React.Component { + constructor(props) { + super(props); + + this.state = {}; + } + + render() { + return ( +
this.handleActorClick(this.props.actor)}> +
+ {this.props.actor.thumbnail === null ? : 'dfdf' /* todo render picture provided here! */} +
+
{this.props.actor.name}
+
+ ); + } + + /** + * event handling for actor tile click + */ + handleActorClick(actor) { + // if clicklistender is defined use this one + if (this.props.onClick) { + this.props.onClick(actor.id); + return; + } + + // Redirect to actor page + GlobalInfos.getViewBinding().changeRootElement(); + } +} + +export default ActorTile; diff --git a/src/elements/ActorTile/ActorTile.module.css b/src/elements/ActorTile/ActorTile.module.css new file mode 100644 index 0000000..59e596b --- /dev/null +++ b/src/elements/ActorTile/ActorTile.module.css @@ -0,0 +1,29 @@ +.actortile { + background-color: #179017; + border-radius: 10px; + cursor: pointer; + float: left; + height: 200px; + margin: 3px; + width: 130px; + + transition: opacity ease 0.5s; +} + +.actortile:hover{ + opacity: 0.7; + transition: opacity ease 0.5s; +} + +.actortile_thumbnail { + color: #c6c6c6; + height: 160px; + margin-top: 10px; + text-align: center; +} + +.actortile_name { + color: white; + text-align: center; + /*todo dynamic text coloring dependent on theme*/ +} diff --git a/src/elements/ActorTile/ActorTile.test.js b/src/elements/ActorTile/ActorTile.test.js new file mode 100644 index 0000000..e2c5ab3 --- /dev/null +++ b/src/elements/ActorTile/ActorTile.test.js @@ -0,0 +1,21 @@ +import {shallow} from 'enzyme'; +import React from 'react'; +import ActorTile from './ActorTile'; + +describe('', function () { + it('renders without crashing ', function () { + const wrapper = shallow(); + wrapper.unmount(); + }); + + it('simulate click', function () { + const wrapper = shallow(); + + const func = jest.fn(); + prepareViewBinding(func); + + wrapper.simulate('click'); + + expect(func).toBeCalledTimes(1); + }); +}); diff --git a/src/elements/AddTagPopup/AddTagPopup.js b/src/elements/AddTagPopup/AddTagPopup.js deleted file mode 100644 index 1d165e6..0000000 --- a/src/elements/AddTagPopup/AddTagPopup.js +++ /dev/null @@ -1,157 +0,0 @@ -import React from 'react'; -import ReactDom from 'react-dom'; -import style from './AddTagPopup.module.css'; -import Tag from '../Tag/Tag'; -import {Line} from '../PageTitle/PageTitle'; -import GlobalInfos from '../../GlobalInfos'; - -/** - * component creates overlay to add a new tag to a video - */ -class AddTagPopup extends React.Component { - /// instance of root element - element; - - constructor(props, context) { - super(props, context); - - this.state = {items: []}; - this.handleClickOutside = this.handleClickOutside.bind(this); - this.keypress = this.keypress.bind(this); - - this.props = props; - } - - componentDidMount() { - document.addEventListener('click', this.handleClickOutside); - document.addEventListener('keyup', this.keypress); - - // add element drag drop events - if (this.element != null) { - this.dragElement(); - } - - const updateRequest = new FormData(); - updateRequest.append('action', 'getAllTags'); - - fetch('/api/tags.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json()) - .then((result) => { - this.setState({ - items: result - }); - }); - } - - componentWillUnmount() { - // remove the appended listeners - document.removeEventListener('click', this.handleClickOutside); - document.removeEventListener('keyup', this.keypress); - } - - render() { - const themeStyle = GlobalInfos.getThemeStyle(); - return ( -
this.element = el}> -
Add a Tag to this Video:
- -
- {this.state.items ? - this.state.items.map((i) => ( - { - this.addTag(i.tag_id, i.tag_name); - }}>{i.tag_name} - )) : null} -
-
- ); - } - - /** - * Alert if clicked on outside of element - */ - handleClickOutside(event) { - const domNode = ReactDom.findDOMNode(this); - - if (!domNode || !domNode.contains(event.target)) { - this.props.onHide(); - } - } - - /** - * key event handling - * @param event keyevent - */ - keypress(event) { - // hide if escape is pressed - if (event.key === 'Escape') { - this.props.onHide(); - } - } - - /** - * add a new tag to this video - * @param tagid tag id to add - * @param tagname tag name to add - */ - addTag(tagid, tagname) { - console.log(this.props); - const updateRequest = new FormData(); - updateRequest.append('action', 'addTag'); - updateRequest.append('id', tagid); - updateRequest.append('movieid', this.props.movie_id); - - fetch('/api/tags.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json() - .then((result) => { - if (result.result !== 'success') { - console.log('error occured while writing to db -- todo error handling'); - console.log(result.result); - } else { - this.props.submit(tagid, tagname); - } - this.props.onHide(); - })); - } - - /** - * make the element drag and droppable - */ - dragElement() { - let xOld = 0, yOld = 0; - - const elmnt = this.element; - elmnt.firstChild.onmousedown = dragMouseDown; - - - function dragMouseDown(e) { - e.preventDefault(); - // get the mouse cursor position at startup: - xOld = e.clientX; - yOld = e.clientY; - document.onmouseup = closeDragElement; - // call a function whenever the cursor moves: - document.onmousemove = elementDrag; - } - - function elementDrag(e) { - e.preventDefault(); - // calculate the new cursor position: - const dx = xOld - e.clientX; - const dy = yOld - e.clientY; - xOld = e.clientX; - yOld = e.clientY; - // set the element's new position: - elmnt.style.top = (elmnt.offsetTop - dy) + 'px'; - elmnt.style.left = (elmnt.offsetLeft - dx) + 'px'; - } - - function closeDragElement() { - // stop moving when mouse button is released: - document.onmouseup = null; - document.onmousemove = null; - } - } -} - -export default AddTagPopup; diff --git a/src/elements/NewTagPopup/NewTagPopup.js b/src/elements/NewTagPopup/NewTagPopup.js deleted file mode 100644 index 8f4d142..0000000 --- a/src/elements/NewTagPopup/NewTagPopup.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import Modal from 'react-bootstrap/Modal'; -import {Form} from 'react-bootstrap'; - -/** - * creates modal overlay to define a new Tag - */ -class NewTagPopup extends React.Component { - constructor(props, context) { - super(props, context); - - this.props = props; - } - - render() { - return ( - <> - - - - Create a new Tag! - - - - - Tag Name: - { - this.value = v.target.value; - }}/> - - This Tag will automatically show up on category page. - - - - - - - - - ); - } - - /** - * store the filled in form to the backend - */ - storeselection() { - const updateRequest = new FormData(); - updateRequest.append('action', 'createTag'); - updateRequest.append('tagname', this.value); - - fetch('/api/tags.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json()) - .then((result) => { - if (result.result !== 'success') { - console.log('error occured while writing to db -- todo error handling'); - console.log(result.result); - } - this.props.onHide(); - }); - } -} - -export default NewTagPopup; diff --git a/src/elements/Popups/AddActorPopup/AddActorPopup.js b/src/elements/Popups/AddActorPopup/AddActorPopup.js new file mode 100644 index 0000000..77d5e95 --- /dev/null +++ b/src/elements/Popups/AddActorPopup/AddActorPopup.js @@ -0,0 +1,102 @@ +import PopupBase from '../PopupBase'; +import React from 'react'; +import ActorTile from '../../ActorTile/ActorTile'; +import style from './AddActorPopup.module.css'; +import {NewActorPopupContent} from '../NewActorPopup/NewActorPopup'; + +/** + * Popup for Adding a new Actor to a Video + */ +class AddActorPopup extends React.Component { + constructor(props) { + super(props); + + this.state = { + contentDefault: true, + actors: undefined + }; + + this.tileClickHandler = this.tileClickHandler.bind(this); + } + + render() { + return ( + <> + {/* todo render actor tiles here and add search field*/} + {this.setState({contentDefault: false});}}>Create new Actor}> + {this.resolvePage()} + + + ); + } + + componentDidMount() { + // fetch the available actors + this.loadActors(); + } + + /** + * selector for current showing popup page + * @returns {JSX.Element} + */ + resolvePage() { + if (this.state.contentDefault) return (this.getContent()); + else return ( { + this.loadActors(); + this.setState({contentDefault: true}); + }}/>); + } + + /** + * returns content for the newActor popup + * @returns {JSX.Element} + */ + getContent() { + if (this.state.actors) { + return (
+ {this.state.actors.map((el) => ())} +
); + } else { + return (
somekind of loading
); + } + } + + /** + * event handling for ActorTile Click + */ + tileClickHandler(actorid) { + // fetch the available actors + const req = new FormData(); + req.append('action', 'addActorToVideo'); + req.append('actorid', actorid); + req.append('videoid', this.props.movie_id); + + fetch('/api/actor.php', {method: 'POST', body: req}) + .then((response) => response.json() + .then((result) => { + if (result.result === 'success') { + // return back to player page + this.props.onHide(); + } else { + console.error('an error occured while fetching actors'); + console.error(result); + } + })); + } + + loadActors() { + const req = new FormData(); + req.append('action', 'getAllActors'); + + fetch('/api/actor.php', {method: 'POST', body: req}) + .then((response) => response.json() + .then((result) => { + this.setState({actors: result}); + })); + } +} + +export default AddActorPopup; diff --git a/src/elements/Popups/AddActorPopup/AddActorPopup.module.css b/src/elements/Popups/AddActorPopup/AddActorPopup.module.css new file mode 100644 index 0000000..a9b1224 --- /dev/null +++ b/src/elements/Popups/AddActorPopup/AddActorPopup.module.css @@ -0,0 +1,11 @@ +.newactorbutton { + border-radius: 5px; + border-width: 0; + color: white; + margin-right: 15px; + margin-top: 12px; + padding: 6px; + background-color: green; + width: 140px; + width: 140px; +} diff --git a/src/elements/Popups/AddActorPopup/AddActorPopup.test.js b/src/elements/Popups/AddActorPopup/AddActorPopup.test.js new file mode 100644 index 0000000..f5294ad --- /dev/null +++ b/src/elements/Popups/AddActorPopup/AddActorPopup.test.js @@ -0,0 +1,19 @@ +import {shallow} from 'enzyme'; +import React from 'react'; +import AddActorPopup from './AddActorPopup'; + +describe('', function () { + it('renders without crashing ', function () { + const wrapper = shallow(); + wrapper.unmount(); + }); + + // it('simulate change to other page', function () { + // const wrapper = shallow(); + // + // console.log(wrapper.find('PopupBase').dive().debug()); + // + // + // console.log(wrapper.debug()); + // }); +}); diff --git a/src/elements/Popups/AddTagPopup/AddTagPopup.js b/src/elements/Popups/AddTagPopup/AddTagPopup.js new file mode 100644 index 0000000..9eb8984 --- /dev/null +++ b/src/elements/Popups/AddTagPopup/AddTagPopup.js @@ -0,0 +1,67 @@ +import React from 'react'; +import Tag from '../../Tag/Tag'; +import PopupBase from '../PopupBase'; + +/** + * component creates overlay to add a new tag to a video + */ +class AddTagPopup extends React.Component { + constructor(props, context) { + super(props, context); + + this.state = {items: []}; + } + + componentDidMount() { + const updateRequest = new FormData(); + updateRequest.append('action', 'getAllTags'); + + fetch('/api/tags.php', {method: 'POST', body: updateRequest}) + .then((response) => response.json()) + .then((result) => { + this.setState({ + items: result + }); + }); + } + + render() { + return ( + + {this.state.items ? + this.state.items.map((i) => ( + { + this.addTag(i.tag_id, i.tag_name); + }}>{i.tag_name} + )) : null} + + ); + } + + /** + * add a new tag to this video + * @param tagid tag id to add + * @param tagname tag name to add + */ + addTag(tagid, tagname) { + console.log(this.props); + const updateRequest = new FormData(); + updateRequest.append('action', 'addTag'); + updateRequest.append('id', tagid); + updateRequest.append('movieid', this.props.movie_id); + + fetch('/api/tags.php', {method: 'POST', body: updateRequest}) + .then((response) => response.json() + .then((result) => { + if (result.result !== 'success') { + console.log('error occured while writing to db -- todo error handling'); + console.log(result.result); + } else { + this.props.submit(tagid, tagname); + } + this.props.onHide(); + })); + } +} + +export default AddTagPopup; diff --git a/src/elements/AddTagPopup/AddTagPopup.module.css b/src/elements/Popups/AddTagPopup/AddTagPopup.module.css similarity index 100% rename from src/elements/AddTagPopup/AddTagPopup.module.css rename to src/elements/Popups/AddTagPopup/AddTagPopup.module.css diff --git a/src/elements/AddTagPopup/AddTagPopup.test.js b/src/elements/Popups/AddTagPopup/AddTagPopup.test.js similarity index 100% rename from src/elements/AddTagPopup/AddTagPopup.test.js rename to src/elements/Popups/AddTagPopup/AddTagPopup.test.js diff --git a/src/elements/Popups/NewActorPopup/NewActorPopup.js b/src/elements/Popups/NewActorPopup/NewActorPopup.js new file mode 100644 index 0000000..8e0a85e --- /dev/null +++ b/src/elements/Popups/NewActorPopup/NewActorPopup.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PopupBase from '../PopupBase'; +import style from './NewActorPopup.module.css'; + +/** + * creates modal overlay to define a new Tag + */ +class NewActorPopup extends React.Component { + render() { + return ( + + + + ); + } +} + +export class NewActorPopupContent extends React.Component { + constructor(props, context) { + super(props, context); + + this.props = props; + } + + render() { + return ( + <> +
+ { + this.value = v.target.value; + }}/>
+ + + ); + } + + /** + * store the filled in form to the backend + */ + storeselection() { + // check if user typed in name + if (this.value === '' || this.value === undefined) return; + + const req = new FormData(); + req.append('action', 'createActor'); + req.append('actorname', this.value); + + fetch('/api/actor.php', {method: 'POST', body: req}) + .then((response) => response.json()) + .then((result) => { + if (result.result !== 'success') { + console.log('error occured while writing to db -- todo error handling'); + console.log(result.result); + } + this.props.onHide(); + }); + } +} + +export default NewActorPopup; diff --git a/src/elements/Popups/NewActorPopup/NewActorPopup.module.css b/src/elements/Popups/NewActorPopup/NewActorPopup.module.css new file mode 100644 index 0000000..aa477d8 --- /dev/null +++ b/src/elements/Popups/NewActorPopup/NewActorPopup.module.css @@ -0,0 +1,8 @@ +.savebtn { + background-color: greenyellow; + border: 0; + border-radius: 4px; + float: right; + margin-top: 30px; + padding: 3px; +} diff --git a/src/elements/Popups/NewActorPopup/NewActorPopup.test.js b/src/elements/Popups/NewActorPopup/NewActorPopup.test.js new file mode 100644 index 0000000..8e6d461 --- /dev/null +++ b/src/elements/Popups/NewActorPopup/NewActorPopup.test.js @@ -0,0 +1,54 @@ +import React from 'react'; + +import {shallow} from 'enzyme'; +import '@testing-library/jest-dom'; +import NewActorPopup, {NewActorPopupContent} from './NewActorPopup'; + +describe('', function () { + it('renders without crashing ', function () { + const wrapper = shallow(); + wrapper.unmount(); + }); +}); + +describe('', () => { + it('renders without crashing', function () { + const wrapper = shallow(); + wrapper.unmount(); + }); + + it('simulate button click', function () { + const wrapper = shallow(); + + // manually set typed in actorname + wrapper.instance().value = 'testactorname'; + + global.fetch = prepareFetchApi({}); + + expect(global.fetch).toBeCalledTimes(0); + wrapper.find('button').simulate('click'); + + // fetch should have been called once now + expect(global.fetch).toBeCalledTimes(1); + }); + + it('test not allowing request if textfield is empty', function () { + const wrapper = shallow(); + + global.fetch = prepareFetchApi({}); + + expect(global.fetch).toBeCalledTimes(0); + wrapper.find('button').simulate('click'); + + // fetch should not be called now + expect(global.fetch).toBeCalledTimes(0); + }); + + it('test input change', function () { + const wrapper = shallow(); + + wrapper.find('input').simulate('change', {target: {value: 'testinput'}}); + + expect(wrapper.instance().value).toBe('testinput'); + }); +}); diff --git a/src/elements/Popups/NewTagPopup/NewTagPopup.js b/src/elements/Popups/NewTagPopup/NewTagPopup.js new file mode 100644 index 0000000..9d0287c --- /dev/null +++ b/src/elements/Popups/NewTagPopup/NewTagPopup.js @@ -0,0 +1,46 @@ +import React from 'react'; +import PopupBase from '../PopupBase'; +import style from './NewTagPopup.module.css'; + +/** + * creates modal overlay to define a new Tag + */ +class NewTagPopup extends React.Component { + constructor(props, context) { + super(props, context); + + this.props = props; + } + + render() { + return ( + +
{ + this.value = v.target.value; + }}/>
+ +
+ ); + } + + /** + * store the filled in form to the backend + */ + storeselection() { + const updateRequest = new FormData(); + updateRequest.append('action', 'createTag'); + updateRequest.append('tagname', this.value); + + fetch('/api/tags.php', {method: 'POST', body: updateRequest}) + .then((response) => response.json()) + .then((result) => { + if (result.result !== 'success') { + console.log('error occured while writing to db -- todo error handling'); + console.log(result.result); + } + this.props.onHide(); + }); + } +} + +export default NewTagPopup; diff --git a/src/elements/Popups/NewTagPopup/NewTagPopup.module.css b/src/elements/Popups/NewTagPopup/NewTagPopup.module.css new file mode 100644 index 0000000..aa477d8 --- /dev/null +++ b/src/elements/Popups/NewTagPopup/NewTagPopup.module.css @@ -0,0 +1,8 @@ +.savebtn { + background-color: greenyellow; + border: 0; + border-radius: 4px; + float: right; + margin-top: 30px; + padding: 3px; +} diff --git a/src/elements/NewTagPopup/NewTagPopup.test.js b/src/elements/Popups/NewTagPopup/NewTagPopup.test.js similarity index 69% rename from src/elements/NewTagPopup/NewTagPopup.test.js rename to src/elements/Popups/NewTagPopup/NewTagPopup.test.js index e2cabb2..a996fa6 100644 --- a/src/elements/NewTagPopup/NewTagPopup.test.js +++ b/src/elements/Popups/NewTagPopup/NewTagPopup.test.js @@ -11,12 +11,7 @@ describe('', function () { }); it('test storeseletion click event', done => { - const mockSuccessResponse = {}; - const mockJsonPromise = Promise.resolve(mockSuccessResponse); - const mockFetchPromise = Promise.resolve({ - json: () => mockJsonPromise - }); - global.fetch = jest.fn().mockImplementation(() => mockFetchPromise); + global.fetch = prepareFetchApi({}); const func = jest.fn(); @@ -27,7 +22,7 @@ describe('', function () { } }); - wrapper.find('ModalFooter').find('button').simulate('click'); + wrapper.find('button').simulate('click'); expect(global.fetch).toHaveBeenCalledTimes(1); process.nextTick(() => { diff --git a/src/elements/Popups/PopupBase.js b/src/elements/Popups/PopupBase.js new file mode 100644 index 0000000..e720502 --- /dev/null +++ b/src/elements/Popups/PopupBase.js @@ -0,0 +1,122 @@ +import GlobalInfos from '../../GlobalInfos'; +import style from './PopupBase.module.css'; +import {Line} from '../PageTitle/PageTitle'; +import React from 'react'; + +/** + * wrapper class for generic types of popups + */ +class PopupBase extends React.Component { + constructor(props) { + super(props); + + this.state = {items: []}; + + this.wrapperRef = React.createRef(); + + this.handleClickOutside = this.handleClickOutside.bind(this); + this.keypress = this.keypress.bind(this); + + // parse style props + this.framedimensions = { + width: (this.props.width ? this.props.width : undefined), + height: (this.props.height ? this.props.height : undefined) + }; + } + + componentDidMount() { + document.addEventListener('mousedown', this.handleClickOutside); + document.addEventListener('keyup', this.keypress); + + // add element drag drop events + if (this.wrapperRef != null) { + this.dragElement(); + } + } + + componentWillUnmount() { + // remove the appended listeners + document.removeEventListener('mousedown', this.handleClickOutside); + document.removeEventListener('keyup', this.keypress); + } + + render() { + const themeStyle = GlobalInfos.getThemeStyle(); + return ( +
+
+
{this.props.title}
+
{this.props.banner}
+
+ + +
+ {this.props.children} +
+
+ ); + } + + /** + * Alert if clicked on outside of element + */ + handleClickOutside(event) { + if (this.wrapperRef && !this.wrapperRef.current.contains(event.target)) { + this.props.onHide(); + } + } + + /** + * key event handling + * @param event keyevent + */ + keypress(event) { + // hide if escape is pressed + if (event.key === 'Escape') { + this.props.onHide(); + } + } + + /** + * make the element drag and droppable + */ + dragElement() { + let xOld = 0, yOld = 0; + + const elmnt = this.wrapperRef.current; + if(elmnt === null) return; + + elmnt.firstChild.onmousedown = dragMouseDown; + + + function dragMouseDown(e) { + e.preventDefault(); + // get the mouse cursor position at startup: + xOld = e.clientX; + yOld = e.clientY; + document.onmouseup = closeDragElement; + // call a function whenever the cursor moves: + document.onmousemove = elementDrag; + } + + function elementDrag(e) { + e.preventDefault(); + // calculate the new cursor position: + const dx = xOld - e.clientX; + const dy = yOld - e.clientY; + xOld = e.clientX; + yOld = e.clientY; + // set the element's new position: + elmnt.style.top = (elmnt.offsetTop - dy) + 'px'; + elmnt.style.left = (elmnt.offsetLeft - dx) + 'px'; + } + + function closeDragElement() { + // stop moving when mouse button is released: + document.onmouseup = null; + document.onmousemove = null; + } + } +} + +export default PopupBase; diff --git a/src/elements/Popups/PopupBase.module.css b/src/elements/Popups/PopupBase.module.css new file mode 100644 index 0000000..d2335d3 --- /dev/null +++ b/src/elements/Popups/PopupBase.module.css @@ -0,0 +1,43 @@ +.popup { + border: 3px #3574fe solid; + border-radius: 18px; + height: 80%; + left: 20%; + opacity: 0.95; + position: absolute; + top: 10%; + width: 60%; + z-index: 2; +} + +.header{ + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-between; +} + +.title { + cursor: move; + font-size: x-large; + margin-left: 15px; + margin-top: 10px; + opacity: 1; + float: left; + width: 60%; +} + +.banner { + width: 40%; + float: left; + display: flex; + flex-direction: row; + justify-content: flex-end; +} + +.content { + margin-left: 20px; + margin-right: 20px; + margin-top: 10px; + opacity: 1; +} diff --git a/src/elements/Popups/PopupBase.test.js b/src/elements/Popups/PopupBase.test.js new file mode 100644 index 0000000..9e87994 --- /dev/null +++ b/src/elements/Popups/PopupBase.test.js @@ -0,0 +1,12 @@ +import {shallow} from "enzyme"; +import React from "react"; +import PopupBase from "./PopupBase"; + +describe('', function () { + it('renders without crashing ', function () { + const wrapper = shallow(); + wrapper.unmount(); + }); + + +}); diff --git a/src/elements/Preview/Preview.js b/src/elements/Preview/Preview.js index 2e7dd9e..0e58509 100644 --- a/src/elements/Preview/Preview.js +++ b/src/elements/Preview/Preview.js @@ -64,10 +64,8 @@ class Preview extends React.Component { itemClick() { console.log('item clicked!' + this.state.name); - this.props.viewbinding.changeRootElement( - ); + GlobalInfos.getViewBinding().changeRootElement( + ); } } diff --git a/src/elements/Preview/Previw.test.js b/src/elements/Preview/Previw.test.js index 08a1e21..407dcb8 100644 --- a/src/elements/Preview/Previw.test.js +++ b/src/elements/Preview/Previw.test.js @@ -20,13 +20,7 @@ describe('', function () { const func = jest.fn(); const wrapper = shallow(); - wrapper.setProps({ - viewbinding: { - changeRootElement: () => { - func(); - } - } - }); + prepareViewBinding(func); wrapper.find('.videopreview').simulate('click'); diff --git a/src/elements/Tag/Tag.js b/src/elements/Tag/Tag.js index f77abeb..c8e4201 100644 --- a/src/elements/Tag/Tag.js +++ b/src/elements/Tag/Tag.js @@ -2,6 +2,7 @@ import React from 'react'; import styles from './Tag.module.css'; import CategoryPage from '../../pages/CategoryPage/CategoryPage'; +import GlobalInfos from '../../GlobalInfos'; /** * A Component representing a single Category tag @@ -18,18 +19,16 @@ class Tag extends React.Component { * click handling for a Tag */ TagClick() { + const tag = this.props.children.toString().toLowerCase(); + if (this.props.onclick) { - this.props.onclick(); + this.props.onclick(tag); return; } - const tag = this.props.children.toString().toLowerCase(); - // call callback functin to switch to category page with specified tag - this.props.viewbinding.changeRootElement( - ); + GlobalInfos.getViewBinding().changeRootElement( + ); } } diff --git a/src/elements/Tag/Tag.test.js b/src/elements/Tag/Tag.test.js index b440921..7717484 100644 --- a/src/elements/Tag/Tag.test.js +++ b/src/elements/Tag/Tag.test.js @@ -16,14 +16,11 @@ describe('', function () { }); it('click event triggered and setvideo callback called', function () { - global.fetch = global.prepareFetchApi({}); + global.fetch = prepareFetchApi({}); const func = jest.fn(); - const elem = { - changeRootElement: () => func() - }; + prepareViewBinding(func); - const wrapper = shallow(test); + const wrapper = shallow(test); expect(func).toBeCalledTimes(0); diff --git a/src/elements/VideoContainer/VideoContainer.js b/src/elements/VideoContainer/VideoContainer.js index 75ae8d9..cb9ee52 100644 --- a/src/elements/VideoContainer/VideoContainer.js +++ b/src/elements/VideoContainer/VideoContainer.js @@ -34,8 +34,7 @@ class VideoContainer extends React.Component { + movie_id={elem.movie_id}/> ))} {/*todo css for no items to show*/} {this.state.loadeditems.length === 0 ? diff --git a/src/index.js b/src/index.js index 7384bc7..153a539 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; +// don't allow console logs within production env +// don't allow console logs within production env +global.console.log = process.env.NODE_ENV !== "development" ? (s) => {} : global.console.log; + ReactDOM.render( diff --git a/src/pages/ActorPage/ActorPage.js b/src/pages/ActorPage/ActorPage.js new file mode 100644 index 0000000..8aa825f --- /dev/null +++ b/src/pages/ActorPage/ActorPage.js @@ -0,0 +1,57 @@ +import React from 'react'; +import PageTitle from '../../elements/PageTitle/PageTitle'; +import SideBar, {SideBarTitle} from '../../elements/SideBar/SideBar'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faUser} from '@fortawesome/free-solid-svg-icons'; +import style from './ActorPage.module.css'; +import VideoContainer from '../../elements/VideoContainer/VideoContainer'; + +class ActorPage extends React.Component { + constructor(props) { + super(props); + + this.state = {data: undefined}; + } + + render() { + return ( + <> + + +
+ +
+ Attention: This is an early preview! +
+ {this.state.data ? + : +
No Data found!
} + + ); + } + + componentDidMount() { + this.getActorInfo(); + } + + /** + * request more actor info from backend + */ + getActorInfo() { + // todo 2020-12-4: fetch to db + + const req = new FormData(); + req.append('action', 'getActorInfo'); + req.append('actorid', this.props.actor.actor_id); + + fetch('/api/actor.php', {method: 'POST', body: req}) + .then((response) => response.json() + .then((result) => { + console.log(result); + this.setState({data: result.videos ? result.videos : []}); + })); + } +} + +export default ActorPage; diff --git a/src/pages/ActorPage/ActorPage.module.css b/src/pages/ActorPage/ActorPage.module.css new file mode 100644 index 0000000..4923c46 --- /dev/null +++ b/src/pages/ActorPage/ActorPage.module.css @@ -0,0 +1,4 @@ +.pic { + text-align: center; + margin-bottom: 25px; +} diff --git a/src/pages/ActorPage/ActorPage.test.js b/src/pages/ActorPage/ActorPage.test.js new file mode 100644 index 0000000..e665cbf --- /dev/null +++ b/src/pages/ActorPage/ActorPage.test.js @@ -0,0 +1,12 @@ +import {shallow} from 'enzyme'; +import React from 'react'; +import ActorPage from './ActorPage'; + +describe('', function () { + it('renders without crashing ', function () { + const wrapper = shallow(); + wrapper.unmount(); + }); + + +}); diff --git a/src/pages/CategoryPage/CategoryPage.js b/src/pages/CategoryPage/CategoryPage.js index 0371a50..de320f2 100644 --- a/src/pages/CategoryPage/CategoryPage.js +++ b/src/pages/CategoryPage/CategoryPage.js @@ -4,7 +4,7 @@ import Tag from '../../elements/Tag/Tag'; import videocontainerstyle from '../../elements/VideoContainer/VideoContainer.module.css'; import {TagPreview} from '../../elements/Preview/Preview'; -import NewTagPopup from '../../elements/NewTagPopup/NewTagPopup'; +import NewTagPopup from '../../elements/Popups/NewTagPopup/NewTagPopup'; import PageTitle, {Line} from '../../elements/PageTitle/PageTitle'; import VideoContainer from '../../elements/VideoContainer/VideoContainer'; @@ -44,26 +44,10 @@ class CategoryPage extends React.Component { Default Tags: - { - this.loadTag(e.props.category); - } - }}>All - { - this.loadTag(e.props.category); - } - }}>FullHd - { - this.loadTag(e.props.category); - } - }}>LowQuality - { - this.loadTag(e.props.category); - } - }}>HD + {this.loadTag(tag);}}>All + {this.loadTag(tag);}}>FullHd + {this.loadTag(tag);}}>LowQuality + {this.loadTag(tag);}}>HD @@ -95,7 +78,6 @@ class CategoryPage extends React.Component { key={m.tag_name} name={m.tag_name} tag_id={m.tag_id} - viewbinding={this.props.viewbinding} categorybinding={this.loadTag}/> )) : 'loading'} @@ -105,6 +87,7 @@ class CategoryPage extends React.Component { {this.state.popupvisible ? { + console.error("setstatecalled!"); this.setState({popupvisible: false}); this.loadTags(); }}/> : diff --git a/src/pages/HomePage/HomePage.js b/src/pages/HomePage/HomePage.js index c9fddbd..fd3dcab 100644 --- a/src/pages/HomePage/HomePage.js +++ b/src/pages/HomePage/HomePage.js @@ -158,8 +158,7 @@ class HomePage extends React.Component { {this.state.data.length !== 0 ? : + data={this.state.data}/> :
No Data found!
}
diff --git a/src/pages/Player/Player.js b/src/pages/Player/Player.js index e0e902d..2a53eaf 100644 --- a/src/pages/Player/Player.js +++ b/src/pages/Player/Player.js @@ -6,8 +6,13 @@ import plyrstyle from 'plyr-react/dist/plyr.css' import {Plyr} from 'plyr-react'; import SideBar, {SideBarItem, SideBarTitle} from '../../elements/SideBar/SideBar'; import Tag from '../../elements/Tag/Tag'; -import AddTagPopup from '../../elements/AddTagPopup/AddTagPopup'; +import AddTagPopup from '../../elements/Popups/AddTagPopup/AddTagPopup'; import PageTitle, {Line} from '../../elements/PageTitle/PageTitle'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faPlusCircle} from '@fortawesome/free-solid-svg-icons'; +import AddActorPopup from '../../elements/Popups/AddActorPopup/AddActorPopup'; +import ActorTile from '../../elements/ActorTile/ActorTile'; +import GlobalInfos from '../../GlobalInfos'; /** @@ -44,13 +49,15 @@ class Player extends React.Component { length: null, tags: [], suggesttag: [], - popupvisible: false + popupvisible: false, + actorpopupvisible: false }; this.quickAddTag = this.quickAddTag.bind(this); } componentDidMount() { + // initial fetch of current movie data this.fetchMovieData(); } @@ -111,14 +118,18 @@ class Player extends React.Component { return ( <> {this.state.popupvisible ? - { - this.setState({popupvisible: false}); - }} + {this.setState({popupvisible: false});}} submit={this.quickAddTag} movie_id={this.state.movie_id}/> : null } + { + this.state.actorpopupvisible ? + { + this.refetchActors(); + this.setState({actorpopupvisible: false}); + }} movie_id={this.state.movie_id}/> : null + } ); } @@ -135,14 +146,11 @@ class Player extends React.Component { {this.state.quality !== 0 ? {this.state.quality}p Quality! : null} {this.state.length !== 0 ? - {Math.round(this.state.length / 60)} Minutes of - length! : null} + {Math.round(this.state.length / 60)} Minutes of length! : null} Tags: {this.state.tags.map((m) => ( - {m.tag_name} + {m.tag_name} ))} Tag Quickadd: @@ -176,7 +184,9 @@ class Player extends React.Component { options={this.options}/> :
not loaded yet
}
- + @@ -185,6 +195,24 @@ class Player extends React.Component { }}>Delete Video
+ {/* rendering of actor tiles */} +
+ {this.state.actors ? + this.state.actors.map((actr) => ( + + )) : <> + } +
{ + this.addActor(); + }}> +
+ +
+
Add Actor
+
+
{ @@ -224,7 +252,8 @@ class Player extends React.Component { quality: result.quality, length: result.length, tags: result.tags, - suggesttag: result.suggesttag + suggesttag: result.suggesttag, + actors: result.actors }); console.log(this.state); }); @@ -257,7 +286,7 @@ class Player extends React.Component { * calls callback to viewbinding to show previous page agains */ closebtn() { - this.props.viewbinding.returnToLastElement(); + GlobalInfos.getViewBinding().returnToLastElement(); } /** @@ -273,13 +302,34 @@ class Player extends React.Component { .then((result) => { if (result.result === 'success') { // return to last element if successful - this.props.viewbinding.returnToLastElement(); + GlobalInfos.getViewBinding().returnToLastElement(); } else { console.error('an error occured while liking'); console.error(result); } })); } + + /** + * show the actor add popup + */ + addActor() { + this.setState({actorpopupvisible: true}); + } + + refetchActors() { + const req = new FormData(); + req.append('action', 'getActorsOfVideo'); + req.append('videoid', this.props.movie_id); + + console.log('refrething actors'); + + fetch('/api/actor.php', {method: 'POST', body: req}) + .then((response) => response.json() + .then((result) => { + this.setState({actors: result}); + })); + } } export default Player; diff --git a/src/pages/Player/Player.module.css b/src/pages/Player/Player.module.css index 269c23f..36b5bd0 100644 --- a/src/pages/Player/Player.module.css +++ b/src/pages/Player/Player.module.css @@ -20,10 +20,36 @@ margin-top: 15px; } -.button { - padding: 6px; - border-radius: 5px; - margin-right: 15px; - color: white; - border-width: 0; +.actorcontainer { + margin-top: 13px; +} + +.button { + border-radius: 5px; + border-width: 0; + color: white; + margin-right: 15px; + padding: 6px; +} + +.actorAddTile { + float: left; + padding-left: 25px; + padding-top: 50px; + cursor: pointer; + color: white; + transition: opacity ease 0.5s; +} + +.actorAddTile:hover{ + opacity: 0.7; + transition: opacity ease 0.5s; +} + +.actorAddTile_thumbnail { + height: 90px; +} + +.actorAddTile_name { + bottom: 0; } diff --git a/src/pages/Player/Player.test.js b/src/pages/Player/Player.test.js index 5a59fef..135e3b8 100644 --- a/src/pages/Player/Player.test.js +++ b/src/pages/Player/Player.test.js @@ -78,9 +78,11 @@ describe('', function () { }); it('test delete button', done => { - const wrapper = shallow(); + const wrapper = shallow(); + + const func = jest.fn(); + prepareViewBinding(func) + global.fetch = prepareFetchApi({result: 'success'}); wrapper.find('.videoactions').find('button').at(2).simulate('click'); @@ -88,7 +90,7 @@ describe('', function () { process.nextTick(() => { // refetch is called so fetch called 3 times expect(global.fetch).toHaveBeenCalledTimes(1); - expect(wrapper.instance().props.viewbinding.returnToLastElement).toHaveBeenCalledTimes(1); + expect(func).toHaveBeenCalledTimes(1); global.fetch.mockClear(); done(); @@ -97,15 +99,9 @@ describe('', function () { it('hide click ', function () { const wrapper = shallow(); - const func = jest.fn(); - wrapper.setProps({ - viewbinding: { - returnToLastElement: () => { - func(); - } - } - }); + const func = jest.fn(); + prepareViewBinding(func); expect(func).toHaveBeenCalledTimes(0); wrapper.find('.closebutton').simulate('click'); diff --git a/src/pages/RandomPage/RandomPage.js b/src/pages/RandomPage/RandomPage.js index 0502817..0ddfa01 100644 --- a/src/pages/RandomPage/RandomPage.js +++ b/src/pages/RandomPage/RandomPage.js @@ -31,16 +31,13 @@ class RandomPage extends React.Component { Visible Tags: {this.state.tags.map((m) => ( - {m.tag_name} + {m.tag_name} ))} {this.state.videos.length !== 0 ? + data={this.state.videos}>
diff --git a/src/pages/SettingsPage/GeneralSettings.module.css b/src/pages/SettingsPage/GeneralSettings.module.css index 16106c5..7045c21 100644 --- a/src/pages/SettingsPage/GeneralSettings.module.css +++ b/src/pages/SettingsPage/GeneralSettings.module.css @@ -13,13 +13,13 @@ flex-wrap: wrap; } -.footer{ +.footer { + bottom: 3px; + color: white; + opacity: 0.8; position: fixed; right: 0; - bottom: 3px; width: 110px; - opacity: 0.8; - color: white; } /* On screens that are 722px wide or less, make the columns stack on top of each other instead of next to each other */ diff --git a/src/setupTests.js b/src/setupTests.js index 80a5a3b..2f52b1f 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -6,6 +6,7 @@ import '@testing-library/jest-dom/extend-expect'; import {configure} from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; +import GlobalInfos from './GlobalInfos'; configure({adapter: new Adapter()}); @@ -31,3 +32,16 @@ global.prepareFailingFetchApi = () => { const mockFetchPromise = Promise.reject('myreason'); return (jest.fn().mockImplementation(() => mockFetchPromise)); }; + +/** + * prepares a viewbinding instance + * @param func a mock function to be called + */ +global.prepareViewBinding = (func) => { + GlobalInfos.getViewBinding = () => { + return { + changeRootElement: func, + returnToLastElement: func + } + }; +}