diff --git a/.gitignore b/.gitignore index e6b6e69..133a96a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ /javascript/dev/ /javascript/build/ /node_modules/ +package-lock.json +/nbproject/ +/assets.json \ No newline at end of file diff --git a/Module.php b/Module.php index ce680df..9a335ab 100644 --- a/Module.php +++ b/Module.php @@ -7,6 +7,7 @@ namespace slideshow; use slideshow\Factory\NavBar; +use slideshow\Factory\Home; use Canopy\Request; use Canopy\Response; use Canopy\Server; @@ -21,7 +22,7 @@ public function __construct() parent::__construct(); $this->loadDefines(); $this->setTitle('slideshow'); - $this->setProperName('SlideShow'); + $this->setProperName('Slideshow'); spl_autoload_register('\slideshow\Module::autoloader', true, true); } @@ -39,7 +40,7 @@ public function getController(Request $request) } catch (\Exception $e) { if (SS_FRIENDLY_ERROR) { \phpws2\Error::log($e); - echo \Layout::wrap('

Uh oh...

An error occurred with SlideShow.

', 'SlideShow Error', true); + echo \Layout::wrap('

Uh oh...

An error occurred with Slideshow.

', 'Slideshow Error', true); exit(); } else { throw $e; @@ -55,8 +56,10 @@ private function friendlyController() public function afterRun(Request $request, Response $response) { - \Layout::addStyle('slideshow'); - $this->showNavBar($request); + if (!\PHPWS_Core::atHome()) { + \Layout::addStyle('slideshow'); + $this->showNavBar($request); + } } private function loadDefines() @@ -72,29 +75,22 @@ private function loadDefines() public function runTime(Request $request) { - if (\Current_User::allow('slideshow')) { - NavBar::addItem($this->showList()); - } - if ($request->getModule() !== 'slideshow') { - \Layout::addStyle('slideshow'); + if (\PHPWS_Core::atHome()) { + $content = Home::view(); + \Layout::add($content); + } else if ($request->getModule() !== 'slideshow') { + // PHPCORE::atHome $this->showNavBar($request); } } private function showNavBar(Request $request) { - if ($request->isGet() && !$request->isAjax() && - (\Current_User::allow('slideshow') || \Current_User::allow('users'))) { - + if ($request->isGet() && !$request->isAjax()) { NavBar::view($request); } } - private function showList() - { - return ' Show list'; - } - public static function autoloader($class_name) { static $not_found = array(); diff --git a/README.md b/README.md index e01c334..5417c56 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,33 @@ -# slideshow -It's a show consisting mainly of slides +# Slideshow +Slideshow is a quiz-integrated presentation-based web application designed and maintained by the student developers of [Electronic Student Services](http://ess.appstate.edu) at [Appalachian State University](http://www.appstate.edu) +## Getting Started +### What's needed to run Slideshow on your machine +* The latest version of [Canopy](https://github.com/AppStateESS/canopy) and all that is needed to run it (Docker, etc) +* Up-to-date versions of [Node](https://nodejs.org/en/) & [npm](https://www.npmjs.com/get-npm) +### To Install +* run `git clone https://github.com/AppStateESS/slideshow.git` in Canopy's `mod` directory +* head into Canopy and install the module through Boost +* in the `slideshow` directory, run `npm install` +* Create a `defines.php` based on `defines.dist.php` under `slideshow/config/`. Then change SLIDESHOW_REACT_DEV to `true` and SS_FRIENDLY_ERROR to `false`. +### Helpful Info +* [Canopy Setup Wiki](https://github.com/AppStateESS/slideshow/wiki/Canopy-Installation) +* [Connecting to the Database Wiki](https://github.com/AppStateESS/slideshow/wiki/Connecting-to-the-database-to-view-your-tables.) +## Contributing +Feel free to fork and open a pull request +## Authors +[Contribution View](https://github.com/AppStateESS/slideshow/graphs/contributors) + +This project was originally in development by Matthew McNaney from the [core team at ESS](https://ess.appstate.edu/contact-us). Student developers stripped the project and rewrote it using new technologies. The project is currently guided by Cydney Caldwell. +### Student Rewrite Developers +* [Tyler Craig](https://github.com/AppStateESS/slideshow/commits?author=tylercraig9332) +* [Eric Cambel](https://github.com/AppStateESS/slideshow/commits?author=cambelem) +* [Zack Noble](https://github.com/AppStateESS/slideshow/commits?author=zanoble) +* [Connor Plunkett](https://github.com/connorp987) +## License +Copyright 2019 Electronic Student Services @ Appalachian State University + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/boost/Tables.php b/boost/Tables.php new file mode 100644 index 0000000..16c34b1 --- /dev/null +++ b/boost/Tables.php @@ -0,0 +1,49 @@ + + * @author Tyler Craig + * @license https://opensource.org/licenses/MIT + */ +namespace slideshow; + +use phpws2\Database; + +class Tables +{ + + protected $db; + + public function __construct() + { + $this->db = Database::getDB(); + } + + public function createShow() + { + $show = new \slideshow\Resource\ShowResource; + return $show->createTable($this->db); + } + + public function createSession() + { + $session = new \slideshow\Resource\SessionResource; + return $session->createTable($this->db); + } + + public function createSlide() + { + $slide = new \slideshow\Resource\SlideResource; + return $slide->createTable($this->db); + } + + public function createQuiz() + { + $quiz = new \slideshow\Resource\QuizResource; + return $quiz->createTable($this->db); + } +} diff --git a/boost/boost.php b/boost/boost.php index 5b3db20..ce588c3 100644 --- a/boost/boost.php +++ b/boost/boost.php @@ -13,12 +13,12 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * - * + * @author Tyler Craig * @author Matthew McNaney * @license http://opensource.org/licenses/gpl-3.0.html */ -$proper_name = 'Slidehow'; -$version = '1.0.0'; +$proper_name = 'Slideshow'; +$version = '1.4.4'; $register = false; $unregister = false; $import_sql = false; diff --git a/boost/controlpanel.php b/boost/controlpanel.php index 0c9b0db..fb3c495 100644 --- a/boost/controlpanel.php +++ b/boost/controlpanel.php @@ -23,6 +23,6 @@ 'label' => 'Slideshow', 'restricted' => TRUE, 'url' => 'slideshow/', - 'description' => 'Slideshow', 'Slideshow module using reveal.js.', + 'description' => 'Slideshow', 'Slideshow module', 'image' => 'slideshow.png', 'tab' => 'content'); diff --git a/boost/install.php b/boost/install.php index a73b7f9..1c79287 100644 --- a/boost/install.php +++ b/boost/install.php @@ -2,36 +2,38 @@ /** * @author Matthew McNaney + * @author Tyler Craig */ + +use phpws2\Database; +require_once PHPWS_SOURCE_DIR . 'mod/slideshow/boost/Tables.php'; + function slideshow_install(&$content) { $db = \phpws2\Database::getDB(); $db->begin(); + $show; + $session; + $slide; + $quiz; try { - $ssUserToSection = $db->buildTable('ss_usertosection'); - $ssUserToSection->addDataType('userId', 'int')->setIsNull(true); - $ssUserToSection->addDataType('showId', 'int')->setIsNull(true); - $ssUserToSection->addDataType('sectionId', 'int')->setIsNull(true); - $ssUserToSection->addDataType('currentSlide', 'int')->setIsNull(true)->setDefault(1); - $ssUserToSection->addDataType('complete', 'int')->setIsNull(true)->setDefault(0); - $ssUserToSection->create(); - - $decision = new \slideshow\Resource\DecisionResource; - $decision->createTable($db); - - $section = new \slideshow\Resource\SectionResource; - $section->createTable($db); + $tables = new slideshow\Tables; - $show = new \slideshow\Resource\ShowResource; - $show->createTable($db); + $show = $tables->createShow(); + $session = $tables->createSession(); + $slide = $tables->createSlide(); + $quiz = $tables->createQuiz(); - $slide = new \slideshow\Resource\SlideResource; - $slide->createTable($db); - } catch (\Exception $e) { \phpws2\Error::log($e); $db->rollback(); + + //$show->drop(true); + //$session->drop(true); + //$slide->drop(true); + //$quiz->drop(true); + throw $e; } $db->commit(); diff --git a/boost/uninstall.php b/boost/uninstall.php index 24d020c..acff546 100644 --- a/boost/uninstall.php +++ b/boost/uninstall.php @@ -21,5 +21,22 @@ function slideshow_uninstall(&$content) { - return TRUE; + $db = \phpws2\Database::getDB(); + $db->begin(); + try { + $db->buildTable('ss_show')->drop(); + $db->buildTable('ss_session')->drop(); + $db->buildTable('ss_slide')->drop(); + $db->buildTable('ss_quiz')->drop(); + + } + catch (Exception $e) { + \phpws2\Error::log($e); + throw $e; + } + $db->commit(); + + $content[] = "Tables dropped"; + return true; + } diff --git a/boost/update.php b/boost/update.php new file mode 100644 index 0000000..36de83f --- /dev/null +++ b/boost/update.php @@ -0,0 +1,220 @@ +run(); + return true; +} + +class slideshowUpdate +{ + + private $content; + private $cversion; + + public function __construct($content, $cversion) + { + $this->content = $content; + $this->cversion = $cversion; + } + + public function run() + { + // To add an update, add a case, and don't include a break; + switch (1) { + case $this->compare('1.1.0'): + $this->update('1.1.0'); + case $this->compare('1.2.0'): + $this->update('1.2.0'); + case $this->compare('1.3.0'): + $this->update('1.3.0'); + case $this->compare('1.3.1'): + $this->update('1.3.1'); + case $this->compare('1.3.2'): + $this->update('1.3.2'); + case $this->compare('1.3.3'): + $this->update('1.3.3'); + case $this->compare('1.3.4'): + $this->update('1.3.4'); + case $this->compare('1.4.1'); + $this->update('1.4.1'); + case $this->compare('1.4.2'); + $this->update('1.4.2'); + case $this->compare('1.4.3'); + $this->update('1.4.3'); + case $this->compare('1.4.4'); + $this->update('1.4.4'); + } + } + + private function compare($version) + { + return version_compare($this->cversion, $version, '<'); + } + + private function update($version) + { + $method = 'v' . str_replace('.', '_', $version); + $this->$method(); + } + + private function v1_1_0() + { + $db = Database::getDB(); + $t = $db->addTable('ss_show'); + $dt = new \phpws2\Database\Datatype\Varchar($t, 'content'); + $dt->setDefault(null); + $dt->add(); + + $changes[] = 'content now saves to the database'; + $changes[] = 'content can now be loaded from the database'; + $this->addContent('1.1.0', $changes); + } + + private function v1_2_0() + { + $db = Database::getDB(); + + $session = new \slideshow\Resource\SessionResource; + $session->createTable($db); + + $changes[] = 'can now keep track of user progress through the quizzes they take'; + $this->addContent('1.2.0', $changes); + } + + private function v1_3_0() + { + $db = Database::getDB(); + + // A fresh install is needed, so a drop will be completed lol + $db->buildTable('ss_show')->drop(); + + $show = new \slideshow\Resource\ShowResource; + $show->createTable($db); + + $slide = new \slideshow\Resource\SlideResource; + $slide->createTable($db); + + $changes[] = 'Slide data is pulled out and saved seperately'; + $this->addContent('1.3.0', $changes); + } + + private function v1_3_1() + { + $db = Database::getDB(); + + $tbl = $db->addTable('ss_show'); + $dt = new \phpws2\Variable\SmallInteger($tbl, 'slideTimer'); + $dt->setDefault(2); + $dt->add(); + + + $tbl = $db->addTable('ss_slide'); + $sql = "ALTER TABLE ss_slide ADD backgroundColor varchar(7) DEFAULT '#E5E7E9';"; + $pdo = $db->getPDO(); + $q = $pdo->prepare($sql); + $q->execute(); + } + + private function v1_3_2() + { + $db = Database::getDB(); + + $t = $db->addTable('ss_slide'); + $dt = new Database\Datatype\Varchar($t, 'media'); + $dt->setDefault(null); + $dt->add(); + + $changes[] = 'can now add media to slides'; + $this->addContent('1.3.2', $changes); + } + + private function v1_3_3() + { + $db = Database::getDB(); + + $t = $db->addTable('ss_slide'); + $dt = new Database\Datatype\Varchar($t, 'thumb'); + $dt->setDefault(null); + $dt->add(); + + $changes[] = 'thumbnail support'; + $this->addContent('1.3.3', $changes); + } + + private function v1_3_4() + { + $db = Database::getDB(); + + $t = $db->addTable('ss_show'); + $dt = new Database\Datatype\Varchar($t, 'preview'); + $dt->setDefault(null); + $dt->add(); + + $dt = new Database\Datatype\Boolean($t, 'useThumb'); + $dt->setDefault(false); + $dt->add(); + + $changes[] = 'show preview image'; + $this->addContent('1.3.4', $changes); + } + + private function v1_4_1() + { + + $db = Database::getDB(); + + $quiz = new \slideshow\Resource\QuizResource; + $quiz->createTable($db); + + $changes[] = 'add quiz table'; + $this->addContent('1.4.1', $changes); + } + + private function v1_4_2() + { + $db = Database::getDB(); + $tbl = $db->addTable('ss_slide'); + + $sql = "ALTER TABLE ss_slide CHANGE COLUMN backgroundColor background varchar(255) DEFAULT '#E5E7E9';"; + $pdo = $db->getPDO(); + $q = $pdo->prepare($sql); + $q->execute(); + + $changes[] = 'Background Image Support'; + $this->addContent('1.4.2', $changes); + } + + private function v1_4_3() + { + $changes[] = 'Updated packages. Fixed some errors.'; + $this->addContent('1.4.3', $changes); + } + + private function v1_4_4() + { + $db = Database::getDB(); + $t = $db->addTable('ss_session'); + $dt = new \phpws2\Database\Datatype\Varchar($t, 'ip', 20); + $dt->setDefault(null); + $dt->add(); + $changes[] = 'Allowing unlogged users'; + $this->addContent('1.4.4', $changes); + } + + private function addContent($version, array $changes) + { + $changes_string = implode("\n+ ", $changes); + $this->content[] = << + Version $version + ------------------------------------------------------ + + $changes_string + +EOF; + } + +} diff --git a/class/Controller/BaseController.php b/class/Controller/BaseController.php index 426d088..769b2d7 100644 --- a/class/Controller/BaseController.php +++ b/class/Controller/BaseController.php @@ -43,7 +43,7 @@ public function jsonResponse($json) $response = new \Canopy\Response($view); return $response; } - + private function loadController(\Canopy\Request $request) { @@ -114,9 +114,11 @@ public function get(\Canopy\Request $request) { if ($request->isAjax()) { $result = $this->controller->getJson($request); + } else { $result = $this->controller->getHtml($request); } + return $result; } diff --git a/class/Controller/Decision/Admin.php b/class/Controller/Decision/Admin.php deleted file mode 100644 index 3961528..0000000 --- a/class/Controller/Decision/Admin.php +++ /dev/null @@ -1,63 +0,0 @@ - - * - * @license http://opensource.org/licenses/lgpl-3.0.html - */ - -namespace slideshow\Controller\Decision; - -use Canopy\Request; -use slideshow\Factory\NavBar; - -class Admin extends Base -{ - - /** - * @var \slideshow\Factory\DecisionFactory - */ - protected $factory; - - protected function createPostCommand(Request $request) - { - $slideId = $request->pullPostInteger('slideId'); - $decision = $this->factory->build(); - $decision->title = ''; - $decision->slideId = $slideId; - $decision->sorting = $this->factory->getCurrentSort($slideId) + 1; - $this->factory->save($decision); - return $decision->getStringVars(true); - } - - protected function jsonPatchCommand(Request $request) - { - $this->factory->patch($this->id, $request->pullPatchString('varname'), - $request->pullPatchVar('value')); - $json['success'] = true; - return $json; - } - - protected function deleteCommand(Request $request) - { - $this->factory->delete($this->id); - $json['success'] = true; - return $json; - } - - protected function updatePutCommand(Request $request) - { - $this->factory->put($this->id, $request); - } - -} diff --git a/class/Controller/Quiz/Admin.php b/class/Controller/Quiz/Admin.php new file mode 100644 index 0000000..6c334e8 --- /dev/null +++ b/class/Controller/Quiz/Admin.php @@ -0,0 +1,48 @@ +. +* Connor Plunkett +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ + +namespace slideshow\Controller\Quiz; + +use Canopy\Request; +use slideshow\Factory\Quiz; + +class Admin extends Base +{ + protected function postCommand(Request $request) + { + return $this->factory->post($request); + } + + protected function putCommand(Request $request) + { + return $this->factory->put($request); + } + + protected function deleteCommand(Request $request) { + return $this->factory->delete($request); + } +} \ No newline at end of file diff --git a/class/Controller/Decision/Base.php b/class/Controller/Quiz/Base.php similarity index 67% rename from class/Controller/Decision/Base.php rename to class/Controller/Quiz/Base.php index 4581c56..f66119a 100644 --- a/class/Controller/Decision/Base.php +++ b/class/Controller/Quiz/Base.php @@ -11,28 +11,41 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * - * @author Matthew McNaney + * @author Tyler Craig * * @license http://opensource.org/licenses/lgpl-3.0.html */ -namespace slideshow\Controller\Decision; +namespace slideshow\Controller\Quiz; use Canopy\Request; -use slideshow\Factory\DecisionFactory as Factory; +use slideshow\Factory\QuizFactory as Factory; use slideshow\Controller\RoleController; class Base extends RoleController { /** - * @var object Factory + * @var slideshow\Factory\QuizFactory */ protected $factory; + /** + * + */ + protected $view; + protected function loadFactory() { $this->factory = new Factory; } -} + protected function loadView() + { + + } + + protected function viewJsonCommand(Request $request) { + return $this->factory->get($request); + } +} \ No newline at end of file diff --git a/class/Controller/Quiz/Logged.php b/class/Controller/Quiz/Logged.php new file mode 100644 index 0000000..6c1f659 --- /dev/null +++ b/class/Controller/Quiz/Logged.php @@ -0,0 +1,36 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + namespace slideshow\Controller\Quiz; + + use Canopy\Request; + use slideshow\Factory\QuizFactory; + + + class Logged extends Base + { + + } diff --git a/class/Controller/RoleController.php b/class/Controller/RoleController.php index 4e92ce2..dfd606c 100644 --- a/class/Controller/RoleController.php +++ b/class/Controller/RoleController.php @@ -27,14 +27,18 @@ abstract class RoleController { protected $factory; + protected $view; protected $role; protected $id; abstract protected function loadFactory(); + abstract protected function loadView(); + public function __construct($role) { $this->loadFactory(); + $this->loadView(); $this->role = $role; } @@ -68,12 +72,12 @@ public function post(Request $request) $command = $request->shiftCommand(); if (empty($command)) { - $command = 'create'; + $method_name = 'postCommand'; + } else { + $method_name = $command . 'PostCommand'; } - - $method_name = $command . 'PostCommand'; if (!method_exists($this, $method_name)) { - throw new BadCommand($method_name); + throw new BadCommand(get_class($this) . ':' . $method_name); } $content = $this->$method_name($request); @@ -93,9 +97,15 @@ protected function loadRequestId(Request $request) { $id = $request->shiftCommand(); if (!is_numeric($id)) { - throw new \slideshow\Exception\MissingRequestId($id); + $vars = $request->getRequestVars(); + if (empty($vars['id'])) { + throw new \slideshow\Exception\MissingRequestId($id); + } else { + $this->id = intval($vars['id']); + } + } else { + $this->id = $id; } - $this->id = $id; } public function put(Request $request) @@ -104,12 +114,13 @@ public function put(Request $request) $command = $request->shiftCommand(); if (empty($command)) { - $command = 'update'; + $method_name = 'putCommand'; + } else { + $method_name = $command . 'PutCommand'; } - $method_name = $command . 'PutCommand'; if (!method_exists($this, $method_name)) { - throw new BadCommand($method_name); + throw new BadCommand(get_class($this) . ':' . $method_name); } $content = $this->$method_name($request); @@ -130,7 +141,7 @@ public function getHtml(Request $request) if ($this->id && method_exists($this, 'viewHtmlCommand')) { $method_name = 'viewHtmlCommand'; } else { - throw new BadCommand($method_name); + throw new BadCommand(get_class($this) . ':' . $method_name); } } @@ -150,7 +161,7 @@ public function getJson(Request $request) $method_name = $command . 'JsonCommand'; if (!method_exists($this, $method_name)) { - throw new BadCommand($method_name); + throw new BadCommand(get_class($this) . ':' . $method_name); } $json = $this->$method_name($request); @@ -166,7 +177,8 @@ public function htmlResponse($content) public function jsonResponse($json) { - $view = new \phpws2\View\JsonView($json); + $view = new \phpws2\View\JsonView($json, + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK); $response = new \Canopy\Response($view); return $response; } @@ -195,7 +207,7 @@ public function delete(Request $request) $this->loadRequestId($request); if (!method_exists($this, 'deleteCommand')) { - throw new BadCommand('deleteCommand'); + throw new BadCommand(get_class($this) . ':' . 'deleteCommand'); } $content = $this->deleteCommand($request); diff --git a/class/Controller/Section/Admin.php b/class/Controller/Section/Admin.php deleted file mode 100644 index 9f12c96..0000000 --- a/class/Controller/Section/Admin.php +++ /dev/null @@ -1,59 +0,0 @@ - - * - * @license http://opensource.org/licenses/lgpl-3.0.html - */ - -namespace slideshow\Controller\Section; - -use Canopy\Request; -use slideshow\Factory\NavBar; -use slideshow\Factory\SlideFactory; - -class Admin extends Base -{ - - /** - * @var \slideshow\Factory\SectionFactory - */ - protected $factory; - - public function createPostCommand(Request $request) - { - $section = $this->factory->post($request); - $this->factory->saveResource($section); - $this->factory->createImageDirectory($section); - return true; - } - - protected function viewHtmlCommand(Request $request) - { - $this->addSlideOption($this->id); - return $this->factory->view($this->id); - } - - protected function addSlideOption($id) - { - $item = ' Add a new slide'; - NavBar::addItem($item); - } - - protected function viewJsonCommand(Request $request) - { - $slideFactory = new SlideFactory(); - return $slideFactory->listing($this->id); - } - -} diff --git a/class/Controller/Session/Admin.php b/class/Controller/Session/Admin.php new file mode 100644 index 0000000..7ce5d64 --- /dev/null +++ b/class/Controller/Session/Admin.php @@ -0,0 +1,64 @@ + + * + * @license http://opensource.org/licenses/lgpl-3.0.html + */ + +namespace slideshow\Controller\Session; + +use Canopy\Request; +use slideshow\Factory\SessionFactory; +use slideshow\View\SessionView; + +class Admin extends Base +{ + /** + * @var slideshow\Factory\SessionFactory + */ + protected $factory; + + /** + * @var slideshow\View\SessionView + */ + protected $view; + + /* + * This does nothing because if we are an admin we are demoing the show + */ + protected function putCommand($request) + { + return true; + } + + /* + * This does nothing because if we are an admin we are demoing the show + */ + protected function viewJsonCommand($request) + { + return true; + } + + protected function tableHtmlCommand($request) + { + return $this->view->sessionTable(); + } + + protected function allJsonCommand($request) + { + $sessionData = $this->factory->getAll($request); + return $sessionData; + } + +} diff --git a/class/Controller/Section/Base.php b/class/Controller/Session/Base.php similarity index 73% rename from class/Controller/Section/Base.php rename to class/Controller/Session/Base.php index 5b19717..2a1f2b3 100644 --- a/class/Controller/Section/Base.php +++ b/class/Controller/Session/Base.php @@ -16,30 +16,34 @@ * @license http://opensource.org/licenses/lgpl-3.0.html */ -namespace slideshow\Controller\Section; +namespace slideshow\Controller\Session; use Canopy\Request; -use slideshow\Factory\SectionFactory as Factory; +use slideshow\Factory\SessionFactory as Factory; +use slideshow\View\SessionView as View; use slideshow\Controller\RoleController; class Base extends RoleController { /** - * @var object Factory + * @var slideshow\Factory\SessionFactory */ protected $factory; + /** + * @var slideshow\View\SessionView + */ + protected $view; + protected function loadFactory() { $this->factory = new Factory; } - protected function watchHtmlCommand(Request $request) + protected function loadView() { - $this->loadRequestId($request); - $content = $this->factory->watch($this->id); - echo $content;exit; + $this->view = new View; } } diff --git a/class/Exception/SlideSaveFailure.php b/class/Controller/Session/Logged.php similarity index 68% rename from class/Exception/SlideSaveFailure.php rename to class/Controller/Session/Logged.php index 210992a..2b85c28 100644 --- a/class/Exception/SlideSaveFailure.php +++ b/class/Controller/Session/Logged.php @@ -11,19 +11,22 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * - * @author Matthew McNaney + * @author Tyler Craig * * @license http://opensource.org/licenses/lgpl-3.0.html */ -namespace slideshow\Exception; +namespace slideshow\Controller\Session; -class SlideSaveFailure extends \Exception +use Canopy\Request; +use slideshow\Factory\SessionFactory; + +class Logged extends User { - public function __construct($filename) - { - $this->message = 'Could not save slide'; - } + /** + * @var \slideshow\Factory\SessionFactory + */ + protected $factory; } diff --git a/class/Controller/Session/User.php b/class/Controller/Session/User.php new file mode 100644 index 0000000..551c25e --- /dev/null +++ b/class/Controller/Session/User.php @@ -0,0 +1,34 @@ +factory->put($request); + return true; + } + + protected function viewJsonCommand($request) + { + return $this->factory->get($request); + } + +} diff --git a/class/Controller/Show/Admin.php b/class/Controller/Show/Admin.php index 8452a60..477976b 100644 --- a/class/Controller/Show/Admin.php +++ b/class/Controller/Show/Admin.php @@ -1,62 +1,132 @@ . * - * @author Matthew McNaney + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: * - * @license http://opensource.org/licenses/lgpl-3.0.html + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. */ namespace slideshow\Controller\Show; use Canopy\Request; -use slideshow\Factory\NavBar; +use slideshow\Factory\ShowFactory; +use slideshow\View\ShowView; class Admin extends Base { - public function createPostCommand(Request $request) + /** + * @var \slideshow\Factory\ShowFactory + */ + protected $factory; + + /** + * @var \slideshow\View\ShowView + */ + protected $view; + + /** + * Handles the request to render the list page. + */ + protected function listHtmlCommand(Request $request) + { + return $this->view->adminShow(); + } + + protected function postCommand(Request $request) { return $this->factory->post($request); } - - protected function listHtmlCommand(Request $request) + + protected function patchCommand(Request $request) { - $this->addShowOption(); - $showForm = $this->factory->reactView('showform'); - return parent::listHtmlCommand($request) . $showForm; + $show = $this->factory->patch($request); + return array('show' => $show->getStringVars()); } - protected function viewHtmlCommand(Request $request) + protected function listJsonCommand(Request $request) { - $this->addSectionOption($this->id); - $showId = <<const showId = {$this->id} -EOF; - $sectionForm = $this->factory->reactView('sectionform'); - return parent::viewHtmlCommand($request) . $showId . $sectionForm; + return array('listing' => $this->get($request)); } - - private function addSectionOption($id) + + protected function deleteCommand(Request $request) { - $item = ' Add a new section'; - NavBar::addItem($item); + switch ($request->pullDeleteVar('type')) { + case 'preview': + return $this->factory->deletePreviewImage($this->id); + case 'show': + return $this->factory->delete($this->id); + default: + return $this->factory->delete($this->id); + } } - - private function addShowOption() + + protected function putCommand(Request $request) + { + $this->factory->put($request); + return true; + } + + protected function get() + { + $shows = $this->factory->listing(true); + foreach ($shows as &$show) { + if ($show['useThumb'] == 1) { + $show['preview'] = $this->factory->getFirstPreview($show['id']); + } + } + return $shows; + } + + protected function jsonPatchCommand(Request $request) + { + return $this->factory->patch($request); + } + + protected function previewPostCommand(Request $request) + { + return $this->factory->postPreviewImage($request); + } + + protected function useThumbPostCommand(Request $request) + { + return $this->factory->setUseThumb($request->pullPostVarIfSet('value'), $request->getVar('id')); + } + + protected function getJsonView($data, Request $request) + { + $vars = $request->getRequestVars(); + $command = ''; + if (!empty($data['command'])) { + $command = $data['command']; + } + if ($command == 'getDetails' && \Current_User::allow('slideshow', 'edit')) { + $result = ShowFactory::getDetails($vars['show_id']); + } + return new \phpws2\View\JsonView($result); + } + + protected function presentJsonCommand(Request $request) { - $item = ' Add new show'; - NavBar::addItem($item); + return $this->factory->getShowDetails($request, true); } - } diff --git a/class/Controller/Show/Base.php b/class/Controller/Show/Base.php index 3f220ef..59f2dae 100644 --- a/class/Controller/Show/Base.php +++ b/class/Controller/Show/Base.php @@ -20,29 +20,30 @@ use Canopy\Request; use slideshow\Factory\ShowFactory as Factory; +use slideshow\View\ShowView as View; use slideshow\Controller\RoleController; class Base extends RoleController { /** - * @var object Factory + * @var slideshow\Factory\ShowFactory */ protected $factory; + /** + * @var slideshow\View\ShowView + */ + protected $view; + protected function loadFactory() { $this->factory = new Factory; } - protected function listHtmlCommand(Request $request) - { - return $this->factory->listing(); - } - - protected function viewHtmlCommand(Request $request) + protected function loadView() { - return $this->factory->view($this->id); + $this->view = new View; } } diff --git a/class/Controller/Section/User.php b/class/Controller/Show/Logged.php similarity index 80% rename from class/Controller/Section/User.php rename to class/Controller/Show/Logged.php index 85ae892..ed2cb87 100644 --- a/class/Controller/Section/User.php +++ b/class/Controller/Show/Logged.php @@ -16,9 +16,13 @@ * @license http://opensource.org/licenses/lgpl-3.0.html */ -namespace slideshow\Controller\Section; +namespace slideshow\Controller\Show; -class User extends Base +use Canopy\Request; +use slideshow\Factory\ShowFactory; +use slideshow\View\ShowView; + +class Logged extends User { - + } diff --git a/class/Controller/Show/User.php b/class/Controller/Show/User.php index 2c8d5fb..9fe56a3 100644 --- a/class/Controller/Show/User.php +++ b/class/Controller/Show/User.php @@ -18,7 +18,42 @@ namespace slideshow\Controller\Show; +use Canopy\Request; + class User extends Base { - + + /** + * @var \slideshow\Factory\ShowFactory + */ + protected $factory; + + /** + * @var slideshow\View\ShowView + */ + protected $view; + + /** + * Handles the request to render the list page. + */ + protected function listHtmlCommand(Request $request) + { + return $this->view->show(); + } + + protected function listJsonCommand(Request $request) + { + return array('listing' => $this->factory->listing(false)); + } + + /** + * The user list will not return inactive shows. + * @param Request $request + * @return type + */ + protected function presentJsonCommand(Request $request) + { + return $this->factory->getShowDetails($request, false); + } + } diff --git a/class/Controller/Slide/Admin.php b/class/Controller/Slide/Admin.php index 56e0fee..484eac8 100644 --- a/class/Controller/Slide/Admin.php +++ b/class/Controller/Slide/Admin.php @@ -1,94 +1,104 @@ . * - * @author Matthew McNaney + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: * - * @license http://opensource.org/licenses/lgpl-3.0.html + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. */ namespace slideshow\Controller\Slide; use Canopy\Request; +use slideshow\Factory\Slide; class Admin extends Base { - protected function picturePostCommand(Request $request) + /** + * Renders the view for edit + */ + protected function editHtmlCommand(Request $request) { - return $this->factory->handlePicturePost($request->pullPostInteger('slideId')); + return $this->view->edit(); } - protected function clearPicturePatchCommand(Request $request) + /** + * Returns the slides of a specific slideshow + * @var Canopy\Request + * @return array of slides + */ + protected function editJsonCommand(Request $request) { - return $this->factory->clearBackgroundImage($this->id); + // TODO: look at editJsonCommand in Show + return $this->factory->get($request); } /** - * Creates a new slide. Note, this slide is created without any data and - * passed up to the form. This allows an id to be created. If the form is - * abandoned, the slide should be deleted. - * @param Request $request - * @returns array Array with slide id + * Edits the values for slides -> happens on a save */ - protected function createPostCommand(Request $request) + protected function putCommand(Request $request) { - $slide = $this->factory->post($request); - $slideId = $this->factory->save($slide); - $this->factory->createImageDirectory($slide); - return array('slideId' => $slideId); + return $this->factory->put($request); } protected function deleteCommand(Request $request) { - $this->factory->delete($this->id); + switch ($request->pullDeleteVar('type')) { + case 'all': + return $this->factory->deleteAll($request); + case 'slide': + return $this->factory->deleteSlide($request); + case 'image': + return $this->factory->deleteImage($request); + default: + return; + } } - protected function jsonPatchCommand(Request $request) + protected function imagePostCommand(Request $request) { - $this->factory->patch($this->id, $request->pullPatchString('varname'), - $request->pullPatchVar('value')); - $json['success'] = true; - return $json; + return $this->factory->postImage($request); } - protected function listJsonCommand(Request $request) + protected function thumbPostCommand(Request $request) { - return $this->factory->listingWithDecisions($request->pullGetInteger('sectionId')); + return $this->factory->postThumb($request); } - protected function movePatchCommand(Request $request) + protected function backgroundPostCommand(Request $request) { - $slide = $this->factory->load($this->id); - $this->factory->sort($slide, $request->pullPatchInteger('newPosition')); + return $this->factory->postBackground($request); } - protected function viewJsonCommand(Request $request) + /** + * Renders the view for present + */ + protected function presentHtmlCommand(Request $request) { - $slide = $this->factory->load($this->id); - $view = $slide->getStringVars(); - $view['decisions'] = $this->factory->getDecisions($slide); - $view['content'] = '
' . $view['content'] . '
'; - return $view; + return $this->view->present(); } - protected function editHtmlCommand(Request $request) + protected function presentJsonCommand(Request $request) { - $this->loadRequestId($request); - $slideId = $this->id; - $slideJs = <<const slideId = $slideId; -EOF; - return $slideJs . $this->factory->reactView('Slide'); + return $this->factory->get($request, true); } } diff --git a/class/Controller/Slide/Base.php b/class/Controller/Slide/Base.php index e25fa8b..75c5bbd 100644 --- a/class/Controller/Slide/Base.php +++ b/class/Controller/Slide/Base.php @@ -11,7 +11,7 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * - * @author Matthew McNaney + * @author Tyler Craig * * @license http://opensource.org/licenses/lgpl-3.0.html */ @@ -20,20 +20,32 @@ use Canopy\Request; use slideshow\Factory\SlideFactory as Factory; +use slideshow\View\SlideView as View; use slideshow\Controller\RoleController; class Base extends RoleController { + /** - * @var Factory $factory + * @var \slideshow\Factory\SlideFactory */ protected $factory; - + + /** + * @var \slideshow\View\SlideView + */ + protected $view; + protected function loadFactory() { $this->factory = new Factory; } - + + protected function loadView() + { + $this->view = new View; + } + protected function editHtmlCommand(Request $request) { \Current_User::requireLogin(); diff --git a/class/Controller/Slide/Logged.php b/class/Controller/Slide/Logged.php new file mode 100644 index 0000000..5ed575c --- /dev/null +++ b/class/Controller/Slide/Logged.php @@ -0,0 +1,32 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace slideshow\Controller\Slide; + +class Logged extends User +{ + +} diff --git a/class/Controller/Slide/User.php b/class/Controller/Slide/User.php index dd30384..b33d944 100644 --- a/class/Controller/Slide/User.php +++ b/class/Controller/Slide/User.php @@ -11,14 +11,28 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * - * @author Matthew McNaney * * @license http://opensource.org/licenses/lgpl-3.0.html */ namespace slideshow\Controller\Slide; +use Canopy\Request; + class User extends Base { - + + /** + * Renders the view for present + */ + protected function presentHtmlCommand(Request $request) + { + return $this->view->present(); + } + + protected function presentJsonCommand(Request $request) + { + return $this->factory->get($request, false); + } + } diff --git a/class/Exception/ResourceNotFound.php b/class/Exception/ResourceNotFound.php index f100544..9c4f306 100644 --- a/class/Exception/ResourceNotFound.php +++ b/class/Exception/ResourceNotFound.php @@ -16,7 +16,7 @@ * @license http://opensource.org/licenses/lgpl-3.0.html */ -namespace properties\Exception; +namespace slideshow\Exception; class ResourceNotFound extends \Exception { diff --git a/class/Factory/Base.php b/class/Factory/Base.php index c4c46a8..0443cb4 100644 --- a/class/Factory/Base.php +++ b/class/Factory/Base.php @@ -24,7 +24,8 @@ public function load($id) $resource = $this->build(); $resource->setId($id); if (!parent::loadByID($resource)) { - throw new ResourceNotFound($id); + // Make a new one if it doesn't exist + return $this->build(); } return $resource; } @@ -42,57 +43,4 @@ function($letter) { } } - private function getScript($filename) - { - $root_directory = PHPWS_SOURCE_HTTP . 'mod/slideshow/javascript/'; - if (SLIDESHOW_REACT_DEV) { - $path = "dev/$filename.js"; - } else { - $path = "build/$filename.js"; - } - $script = ""; - return $script; - } - - public function reactView($view_name) - { - static $vendor_included = false; - if (!$vendor_included) { - $script[] = $this->getScript('vendor'); - $vendor_included = true; - } - $script[] = $this->getScript($view_name); - $react = implode("\n", $script); - $content = << -$react -EOF; - return $content; - } - - protected function getRootDirectory() - { - return PHPWS_SOURCE_DIR . 'mod/slideshow/'; - } - - protected function getRootUrl() - { - return PHPWS_SOURCE_HTTP . 'mod/slideshow/'; - } - - - private function getAssetPath($scriptName) - { - $rootDirectory = $this->getRootDirectory(); - if (!is_file($rootDirectory . 'assets.json')) { - exit('Missing assets.json file. Run npm run prod in stories directory.'); - } - $jsonRaw = file_get_contents($rootDirectory . 'assets.json'); - $json = json_decode($jsonRaw, true); - if (!isset($json[$scriptName]['js'])) { - throw new \Exception('Script file not found among assets.'); - } - return $json[$scriptName]['js']; - } - } diff --git a/class/Factory/DecisionFactory.php b/class/Factory/DecisionFactory.php deleted file mode 100644 index f26eac1..0000000 --- a/class/Factory/DecisionFactory.php +++ /dev/null @@ -1,125 +0,0 @@ - - * - * @license http://opensource.org/licenses/lgpl-3.0.html - */ - -namespace slideshow\Factory; - -use slideshow\Resource\DecisionResource as Resource; -use phpws2\Database; -use phpws2\Template; -use Canopy\Request; - -class DecisionFactory extends Base -{ - - public function build() - { - return new Resource; - } - - /** - * - * @param integer $id - * @return Resource - */ - public function load($id) - { - return parent::load($id); - } - - public function listing($slideId) - { - $db = Database::getDB(); - $tbl = $db->addTable('ss_decision'); - $tbl->addFieldConditional('slideId', $slideId); - $tbl->addOrderBy('sorting'); - return $db->select(); - } - - public function continueLink($sectionId, $slideSorting) - { - $next = $slideSorting + 1; - return << -EOF; - } - - public function previousLink($sectionId, $slideSorting) - { - if ($slideSorting == 0) { - return null; - } - $prev = $slideSorting - 1; - return << -EOF; - } - - public function save(Resource $decision) - { - self::saveResource($decision); - return $decision->id; - } - - public function patch($id, $param, $value) - { - static $allowed_params = array('title', 'message', 'lockout', 'next'); - - if (!in_array($param, $allowed_params)) { - throw new \Exception('Parameter may not be patched'); - } - $decision = $this->load($id); - $decision->$param = $value; - $this->save($decision); - return true; - } - - public function getCurrentSort($slideId) - { - $db = Database::getDB(); - $tbl = $db->addTable('ss_decision'); - $tbl->addFieldConditional('slideId', $slideId); - $sorting = $tbl->addField('sorting'); - $tbl->addOrderBy('sorting', 'desc'); - $db->setLimit(1); - return (int) $db->selectColumn(); - } - - public function delete($decisionId) - { - $decision = $this->load($decisionId); - self::deleteResource($decision); - $sortable = new \phpws2\Sortable('ss_decision', 'sorting'); - $sortable->setAnchor('slideId', $decision->slideId); - $sortable->reorder(); - } - - public function put($decisionId, Request $request) - { - $decision = $this->load($decisionId); - $decision->loadPutByType($request); - self::saveResource($decision); - } - - public function link($decision) { - var_dump($decision); - return <<{$decision['title']} -EOF; - } - -} diff --git a/class/Factory/Home.php b/class/Factory/Home.php new file mode 100644 index 0000000..eaf8177 --- /dev/null +++ b/class/Factory/Home.php @@ -0,0 +1,34 @@ + + * + * @license http://opensource.org/licenses/lgpl-3.0.html + */ + +namespace slideshow\Factory; + +class Home +{ + + public static function view() + { + $vars['home_img'] = PHPWS_SOURCE_HTTP . 'mod/slideshow/img/campus.jpg'; + + $template = new \phpws2\Template($vars); + $template->setModuleTemplate('slideshow', 'index.html'); + $content = $template->get(); + return $content; + } + +} diff --git a/class/Factory/NavBar.php b/class/Factory/NavBar.php index 56164a0..3e5a5b4 100644 --- a/class/Factory/NavBar.php +++ b/class/Factory/NavBar.php @@ -23,20 +23,20 @@ class NavBar public static $items; public static $options; - public static $title = 'Administrate'; public static $has_run = false; - public static $halt = false; public static function view(\Canopy\Request $request) { - if (self::$has_run || self::$halt) { + if (self::$has_run) { return; } self::$has_run = true; $auth = \Current_User::getAuthorization(); + $authKey= \Current_User::getAuthKey(); $vars['logged'] = \Current_User::isLogged(); $vars['admin'] = \Current_User::allow('slideshow'); + $vars['is_deity'] = \Current_User::isDeity(); $vars['items'] = null; $vars['options'] = null; @@ -48,12 +48,11 @@ public static function view(\Canopy\Request $request) $vars['options'] = implode('
  • ', self::$options); } - $vars['is_deity'] = \Current_User::isDeity(); $vars['logout_uri'] = $auth->logout_link; + $vars['boost_uri'] = "index.php?module=boost&action=admin&tab=other_mods&authkey=" . $authKey; $vars['username'] = \Current_User::getDisplayName(); $vars['home'] = \Canopy\Server::getSiteUrl(); - $vars['title'] = self::$title; - $vars['current_area'] = self::getNavTitle($request); + $template = new \phpws2\Template($vars); $template->setModuleTemplate('slideshow', 'navbar.html'); $content = $template->get(); @@ -61,23 +60,6 @@ public static function view(\Canopy\Request $request) \Layout::plug($content, 'NAV_LINKS'); } - private static function userLogin() - { - if (!\Current_User::isLogged()) { - $auth = \Current_User::getAuthorization(); - if (!empty($auth->login_link)) { - $url = $auth->login_link; - } else { - $url = 'index.php?module=users&action=user&command=login_page'; - } - NavBar::addItem("Sign in"); - } - } - - private static function getNavTitle(\Canopy\Request $request) - { - return self::$title; - } public static function addItem($item) { @@ -93,14 +75,4 @@ public static function addOption($option, $unshift = false) } } - public static function setTitle($title) - { - self::$title = $title; - } - - public static function halt() - { - self::$halt = true; - } - } diff --git a/class/Factory/QuizFactory.php b/class/Factory/QuizFactory.php new file mode 100644 index 0000000..0863728 --- /dev/null +++ b/class/Factory/QuizFactory.php @@ -0,0 +1,114 @@ +. +* Connor Plunkett +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ + +namespace slideshow\Factory; + +use slideshow\Resource\QuizResource; +use phpws2\Database; +use Canopy\Request; + +class QuizFactory extends Base +{ + protected function build() + { + return new QuizResource; + } + + public function get(Request $request) + { + $vars = $request->getRequestVars(); + $quizId = $vars['Quiz']; + + $sql = 'SELECT * FROM ss_quiz WHERE id=:quizId;'; + $db = Database::getDB(); + $pdo = $db->getPDO(); + $q = $pdo->prepare($sql); + $q->execute(array('quizId'=>$quizId)); + $quiz = $q->fetchAll(); + return $quiz; + } + + public function put(Request $request) + { + $vars = $request->getVars(); + + $resource; + try { + $resource = $this->load($request->pullPutvar('quizId')); + } + catch (\Exception $e) { + var_dump("Resource doesn't exist", $e); + $resource = $this->build(); + } + $resource->question = $request->pullPutVar('question'); + $resource->answers = $request->pullPutVar('answers'); + $resource->correct = $request->pullPutVar('correct'); + $resource->type = $request->pullPutvar('type'); + $resource->feedback = $request->pullPutVar('feedback'); + $this->saveResource($resource); + return $request; + + } + + public function post(Request $request) + { + $resource = $this->build(); + + $this->saveResource($resource); + return $resource->id; + } + + public function delete(Request $request) + { + $vars = $request->getRequestVars(); + $id = $vars['Quiz']; + + $del_type = $request->pullDeleteVarIfSet('type'); + if ($del_type === 'all') { + return $this->deleteAll($id); + } + else { // If there is no type then the request was made from an individual slide. + return $this->deleteOne($id); + } + } + + private function deleteOne($quizId) + { + $resource = $this->load($quizId); + return $this->deleteResource($resource) != 0; + } + + private function deleteAll($showId) + { + $sql = 'DELETE FROM ss_quiz WHERE EXISTS(SELECT quizId FROM ss_slide WHERE showId=:showId);'; + $db = Database::getDB(); + $pdo = $db->getPDO(); + $q = $pdo->prepare($sql); + return $q->execute(array('showId'=>$showId)); + } + +} \ No newline at end of file diff --git a/class/Factory/SectionFactory.php b/class/Factory/SectionFactory.php deleted file mode 100644 index 1d05536..0000000 --- a/class/Factory/SectionFactory.php +++ /dev/null @@ -1,121 +0,0 @@ - - * - * @license http://opensource.org/licenses/lgpl-3.0.html - */ - -namespace slideshow\Factory; - -use slideshow\Resource\SectionResource as Resource; -use phpws2\Database; -use phpws2\Template; -use Canopy\Request; - -class SectionFactory extends Base -{ - - protected function build() - { - return new Resource; - } - - public function post(Request $request) - { - $showFactory = new ShowFactory; - $showId = $request->pullPostInteger('showId'); - $section = $this->build(); - $section->showId = $showId; - $section->title = $request->pullPostString('title'); - $section->sorting = (int) $this->getCurrentSort($showId) + 1; - return $section; - - return true; - } - - public function getCurrentSort($showId) - { - $db = Database::getDB(); - $tbl = $db->addTable('ss_section'); - $tbl->addFieldConditional('showId', $showId); - $sorting = $tbl->addField('sorting'); - $tbl->addOrderBy('sorting', 'desc'); - $db->setLimit(1); - return $db->selectColumn(); - } - - public function listing($showId) - { - $db = Database::getDB(); - $tbl = $db->addTable('ss_section'); - $tbl->addFieldConditional('showId', $showId); - $tbl->addOrderBy('sorting'); - return $db->select(); - } - - public function view($sectionId) - { - $showFactory = new ShowFactory; - $section = $this->load($sectionId); - $show = $showFactory->load($section->showId); - - $vars['sectionId'] = <<const sectionId = {$sectionId} -EOF; - $vars['showTitle'] = $show->title; - $vars['sectionTitle'] = $section->title; - - $vars['react'] = $this->reactView('Section'); - $template = new Template($vars); - $template->setModuleTemplate('slideshow', 'Section/view.html'); - return $template->get(); - } - - public function watch($sectionId) - { - $slideFactory = new SlideFactory; - $slides = $slideFactory->listing($sectionId); - $decisionFactory = new DecisionFactory; - //var_dump($slides);exit; - foreach ($slides as &$slide) { - $decisions = $decisionFactory->listing($slide['id']); -// var_dump($decisions); - if (empty($decisions)) { - $slide['prev'] = $decisionFactory->previousLink($sectionId, - $slide['sorting']); - $slide['next'] = $decisionFactory->continueLink($sectionId, - $slide['sorting']); - } else { - foreach ($decisions as $decision) { - $vars['decisions'][$slide['id']][] = $decisionFactory->link($decision); - } - } - } - $vars['hub'] = PHPWS_SOURCE_HTTP; - $vars['base'] = \Layout::getBase(); - $vars['slides'] = $slides; - $template = new Template($vars); - $template->setModuleTemplate('slideshow', 'Section/watch.html'); - return $template->get(); - } - - public function createImageDirectory($section) - { - $path = $section->getImagePath(); - if (!is_dir($path)) { - mkdir($path); - } - } - -} diff --git a/class/Factory/SessionFactory.php b/class/Factory/SessionFactory.php new file mode 100644 index 0000000..f763c88 --- /dev/null +++ b/class/Factory/SessionFactory.php @@ -0,0 +1,125 @@ + + * + * @license http://opensource.org/licenses/lgpl-3.0.html + */ + +namespace slideshow\Factory; + +use slideshow\Resource\SessionResource as Resource; +use phpws2\Database; +use Canopy\Request; + +class SessionFactory extends Base +{ + + protected function build() + { + return new Resource; + } + + public function get(Request $request) + { + $vars = $request->getRequestVars(); + $showId = intval($vars['Session']); + $userId = \Current_User::getId(); + + $sql = "SELECT id, highestSlide, completed FROM ss_session WHERE userId=:userId AND showId=:showId;"; + $db = Database::getDB(); + $pdo = $db->getPDO(); + $q = $pdo->prepare($sql); + $q->execute(array(':userId' => $userId, ':showId' => $showId)); + $result = $q->fetch(); + + if (empty($result)) { + $this->post($request, $showId); + $this->get($request); + } + return array('highestSlide' => $result[1], 'completed' => $result[2]); + } + + protected function getSessionByUserId(int $showId, int $userId) + { + $db = Database::getDB(); + $tbl = $db->addTable('ss_session'); + $tbl->addFieldConditional('userId', $userId); + $tbl->addFieldConditional('showId', $showId); + return $db->selectOneRow(); + } + + protected function getSessionByIp(int $showId, string $ip) + { + $db = Database::getDB(); + $tbl = $db->addTable('ss_session'); + $tbl->addFieldConditional('showId', $showId); + $tbl->addFieldConditional('ip', $ip); + return $db->selectOneRow(); + } + + public function put(Request $request) + { + $showId = $request->pullGetInteger('Session'); + $highestSlide = (int) $request->pullPutInteger('highestSlide', true); + $completed = (bool) $request->pullPutInteger('completed', true); + + $userId = \Current_User::getId(); + $ip = \Canopy\Server::getUserIp(); + $resource = $this->build(); + + if ($userId > 0) { + $result = $this->getSessionByUserId($showId, $userId); + } else { + $result = $this->getSessionByIp($showId, $ip); + } + + if (empty($result)) { + $resource->showId = $showId; + $resource->userId = $userId; + $resource->ip = $userId == 0 ? $ip : null; + $resource->username = $userId > 0 ? \Current_User::getUsername() : 'anonymous'; + } else { + $resource->setVars($result); + } + if ($highestSlide > $resource->highestSlide) { + $resource->highestSlide = $highestSlide; + } + if (!$resource->completed && $completed) { + $resource->completed = $completed; + } + + if ($resource->userId > 0 || strlen($resource->ip) > 0) { + $this->saveResource($resource); + } else { + throw new \Exception('Cannot save session to database without identifier'); + } + return $resource; + } + + public function getAll(Request $request) + { + $vars = $request->getRequestVars(); + $showId = intval($vars['id']); + + $sql = "SELECT username, highestSlide, completed FROM ss_session WHERE showId=:showId;"; + $db = Database::getDB(); + $pdo = $db->getPDO(); + $q = $pdo->prepare($sql); + $q->execute(array(':showId' => $showId)); + $result = $q->fetchAll(); + + return $result; + } + +} diff --git a/class/Factory/ShowFactory.php b/class/Factory/ShowFactory.php index 69ce600..e8343b9 100644 --- a/class/Factory/ShowFactory.php +++ b/class/Factory/ShowFactory.php @@ -12,58 +12,287 @@ * GNU General Public License for more details. * * @author Matthew McNaney + * @author Tyler Craig * * @license http://opensource.org/licenses/lgpl-3.0.html */ namespace slideshow\Factory; -use slideshow\Resource\ShowResource as Resource; +use slideshow\Resource\ShowResource; +use SlideShow\Factory\SlideFactory; use phpws2\Database; use Canopy\Request; +define('SLIDESHOW_MEDIA_DIRECTORY', 'images/slideshow/'); + class ShowFactory extends Base { protected function build() { - return new Resource; + return new ShowResource; } public function post(Request $request) { - $resource = $this->build(); - $resource->title = $request->pullPostString('title'); + $show = $this->build(); + // Pulls the title from the Post request if it's changed then it will be saved. + $show->title = $request->pullPostString('title'); + $show->active = 0; + //$show->content = []; + $this->saveResource($show); + //$this->createImageDirectory($show); + return $show->id; + } + + public function put(Request $request) + { + // Pull the id from the request: + $vars = $request->getRequestVars(); + $id = intval($vars['Show']); + // Load the resource corresponding to the id from the db: + $resource = $this->load($id); + + // Update/PUT the values that are changed: + // pullPutVarIfSet will return false if not set + $title = $request->pullPutVarIfSet('title'); + $active = $resource->active; + try { + $active = $request->pullPutVar('active'); + } catch (\phpws2\Exception\ValueNotSet $e) { + // putvar was not set for active + } + + $slideTimer = intval($request->pullPutVarIfSet('slideTimer')); + // if any of the vars are set to false we don't need to update them. + if (gettype($title) == "string") { + $resource->title = $title; + } + + $resource->active = $active; + $resource->slideTimer = $slideTimer; + // Save the updated resource to the Database $this->saveResource($resource); - return true; + return $resource; } - public function listing() + /** + * + * Creates a new slideshow upon the patch request. + * @var $showId id of the show to be saved. + */ + public function patch(Request $request) { - $db = Database::getDB(); - $db->addTable('ss_show'); - $tpl['rows'] = $db->select(); - $template = new \phpws2\Template($tpl); - $template->setModuleTemplate('slideshow', 'Show/list.html'); - return $template->get(); + // Pull the id from the request: + $vars = $request->getRequestVars(); + $showId = intval($vars['Show']); + $resource = $this->load($showId); + + $title = $request->pullPatchVarIfSet('title'); + if ($title) { + $resource->title = $title; + } + + $animation = $request->pullPatchVarIfSet('animation'); + if ($animation) { + $resource->animation = $animation; + } + + $active; + try { + $active = $request->pullPatchVar('active'); + } catch (\phpws2\Exception\ValueNotSet $e) { + $active = $resource->active; + } + $resource->active = $active; + + $this->saveResource($resource); + return $resource; } - public function view($id) + /** + * Selects the details about a show from the db + * + * @param $show_id + */ + public static function getDetails($show_id) { - /* @var $resource \slideshow\Resource\ShowResource */ - $resource = $this->load($id); - $sectionFactory = new SectionFactory(); + if (empty($show_id)) { + throw new \Exception("Invalid show id"); + } + + $db = \phpws2\Database::getDB(); + $tbl = $db->addTable('ss_show'); + $tbl->addFieldConditional('id', $show_id); + $show = $db->selectOneRow(); + + return $show; + } + + public function getShows() + { + $db = \phpws2\Database::getDB(); + $tbl = $db->addTable('ss_show'); + $shows = $db->fetchAll(); - $vars = $resource->getStringVars(); - $sections = $sectionFactory->listing($resource->id); - foreach ($sections as $sec) { - $slideFactory = new SlideFactory(); - $slides = $slideFactory->listing($sec['id']); + return $shows; + } + + /** + * + * @param slideshow\Resource\ShowResource $show + */ + public function createImageDirectory($show) + { + $path = $show->getImagePath(); + if (!is_dir($path)) { + mkdir($path); } - $vars['sections'] = $sections; - $template = new \phpws2\Template($vars); + } + + public function getShowDetails($request, $includeInactive = false) + { + // Pull the id from the request: + $vars = $request->getRequestVars(); + $showId = intval($vars['id']); + if ($showId === null || $showId == -1) { + throw new \Exception("ShowId is not valid: $showId", 1); + } + $db = Database::getDB(); + $tbl = $db->addTable('ss_show'); + $tbl->addFieldConditional('id', $showId); + if (!$includeInactive) { + $tbl->addFieldConditional('active', 1); + } + return $db->select(); + } + + public function listing($showAll = false) + { + $db = \phpws2\Database::getDB(); + $tbl = $db->addTable('ss_show'); + $tbl->addOrderBy('title'); + if (!$showAll) { + $tbl->addFieldConditional('active', 1); + } + return $db->select(); + } + + public function view($id) + { + $template = new \phpws2\Template(); $template->setModuleTemplate('slideshow', 'Show/view.html'); return $template->get(); } + public function delete($showId) + { + $returnFlag = true; + $resource = $this->load($showId); + if ($resource->preview != null) { + // this will be set to false if something went wrong + $returnFlag = $this->deletePreviewImage($showId); + } + self::deleteResource($resource); + return $returnFlag; + } + + public function postPreviewImage(Request $request) + { + $vars = $request->getRequestVars(); + $showId = $vars['id']; + if (!empty($showId)) { + $resource = $this->load($showId); + } else { + throw new \Exception("Error loading slideId"); + } + + $path = $this->uploadPreview($_FILES['media'], $showId); + $resource->preview = $path; + $resource->useThumb = false; + $this->saveResource($resource); + return $path; + } + + public function deletePreviewImage($resourceId) + { + $resource = $this->load($resourceId); + $resource->preview = ""; + $resource->useThumb = false; + $this->saveResource($resource); + + return $this->deletePreview($resourceId); + } + + public function setUseThumb($value, $resourceId) + { + $value = ($value === 'true' && gettype($value) !== 'boolean') ? true : false; + $resource = $this->load($resourceId); + $resource->useThumb = $value; + + $path = ''; + + $this->deletePreviewImage($resourceId); + + if ($value) { + $path = $this->getFirstPreview($resourceId); + } + + $this->saveResource($resource); + return $path; + } + + public function getFirstPreview($resourceId) + { + + $sql = 'SELECT thumb FROM ss_slide WHERE showId=:resourceId;'; + $db = Database::getDB(); + $pdo = $db->getPDO(); + $query = $pdo->prepare($sql); + $query->bindParam(':resourceId', $resourceId, \PDO::PARAM_INT); + $query->execute(); + $result = $query->fetch(); + $path = $result['thumb']; + + return json_decode($path); + } + + private function uploadPreview(array $file, $resourceId) + { + + // Check to see if there is a show directory + $master_dir = PHPWS_HOME_DIR . SLIDESHOW_MEDIA_DIRECTORY . 'show/'; + $resource_dir = $master_dir . $resourceId . '/'; + //isdir($master_dir) + // Check to see if there is a directory for the resource + if (is_dir($resource_dir)) { + // If directory exists then we dump it + system('rm -rf ' . escapeshellarg($resource_dir), $ret); + if ($ret != 0) + throw new Exception('Directory Removal Error: ' . $ret); + } + mkdir($resource_dir, 0755, true); + + // upload the image + $dest = $resource_dir . basename($file['name']); + if (move_uploaded_file($file['tmp_name'], $dest)) { + return './' . SLIDESHOW_MEDIA_DIRECTORY . 'show/' . $resourceId . '/' . basename($file['name']); + } else { + var_dump($file); + var_dump($dest); + return "not uploaded and error occured"; + } + } + + private function deletePreview($resourceId) + { + $dir = PHPWS_HOME_DIR . SLIDESHOW_MEDIA_DIRECTORY . 'show/' . $resourceId . '/'; + system('rm -rf ' . escapeshellarg($dir), $ret); + if ($ret != 0) + throw new Exception('Directory Removal Error: ' . $ret); + else + return true; + } + } diff --git a/class/Factory/SlideFactory.php b/class/Factory/SlideFactory.php index 6f625c0..2bed31e 100644 --- a/class/Factory/SlideFactory.php +++ b/class/Factory/SlideFactory.php @@ -1,227 +1,421 @@ . * - * @author Matthew McNaney + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: * - * @license http://opensource.org/licenses/lgpl-3.0.html + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. */ namespace slideshow\Factory; -use slideshow\Resource\SlideResource as Resource; +use slideshow\Resource\SlideResource; use phpws2\Database; use Canopy\Request; +define('SLIDESHOW_MEDIA_DIRECTORY', 'images/slideshow/'); + class SlideFactory extends Base { - private $saveDirectory = './images/slideshow/'; - protected function build() { - return new Resource; + return new SlideResource; } - /** - * - * @param integer $id - * @return \slideshow\Resource\SlideResource - */ - public function load($id) + public function get(Request $request, $includeInactive = false) { - return parent::load($id); + $showId = $request->pullGetInteger('id'); + $slides = $this->getSlides($showId, $includeInactive); + + return array( + 'slides' => $slides + ); } - - public function listing($sectionId) + + public function post(Request $request) { - $db = Database::getDB(); - $tbl = $db->addTable('ss_slide'); - $tbl->addFieldConditional('sectionId', $sectionId); - $tbl->addOrderBy('sorting'); - return $db->select(); + $resource = $this->build(); + + $resource->showId = $request->pullPostVarIfSet('id'); + + $this->saveResource($resource); + return $resource; } - public function listingWithDecisions($sectionId) + /** + * Updates the content of the slide or makes a new one + * @return SlideResource + */ + public function put(Request $request) { - $slides = $this->listing($sectionId); - if (empty($slides)) { - return null; - } - $dFactory = new DecisionFactory; - foreach ($slides as &$slide) { - $slide['decisions'] = $dFactory->listing($slide['id']); - /* - $decisions = $dFactory->listing($slide['id']); - if (empty($decisions)) { - $slide['decisions'] = null; + // Array to return of all the slideIds + $ids = array(); + $quizIds = array(); + + // pull showId from $request + $vars = $request->getRequestVars(); + $showId = intval($vars['Slide']); + + $slides = $request->pullPutVar('slides'); + + $slideIndex = 0; + foreach ($slides as $slide) { + $resource = null; + if (empty($slide['slideId'])) { + $resource = $this->build(); + } else { + $resource = $this->load(intval($slide['slideId'])); + } + + $resource->showId = $showId; + $resource->slideIndex = $slideIndex; + $resource->content = ""; + $isQuiz = $slide['isQuiz'] == 'true' ? true : false; + $resource->isQuiz = $isQuiz; + if ($isQuiz) { + + if (!in_array($slide['quizId'], $quizIds)) { + $resource->quizId = $slide['quizId']; + array_push($quizIds, $slide['quizId']); + + } + var_dump($resource->quizId); } else { - $slide['decisions'] = $decisions; + if (!empty($slide['saveContent'])) { + $resource->content = $slide['saveContent']; + } + } + $resource->background = $slide['background']; + $resource->media = ""; + if (!empty($slide['media'])) { + $resource->media = json_encode($slide['media']); + } + $resource->thumb = ""; + if (!empty($slide['thumb'])) { + $resource->thumb = json_encode($slide['thumb']); } - * - */ + $this->saveResource($resource); + if (!in_array($resource->quizId, $quizIds)) { + array_push($ids, $resource->id); + } + $slideIndex++; } - return $slides; + return $ids; } - public function handlePicturePost($slideId) + public function postImage(Request $request) { - if (!isset($_FILES) || empty($_FILES)) { - return array('error' => 'No files uploaded'); - } - $picture = $_FILES['picture']; - $slide = $this->load($slideId); + $resourceId = intval($request->pullPostVar('slideId')); + try { - $size = getimagesize($picture['tmp_name']); - $result['width'] = $size[0]; - $result['height'] = $size[1]; - $result['path'] = $this->moveImage($picture, $slide); - $result['success'] = true; - } catch (properties\Exception\FileSaveFailure $e) { - $result['success'] = false; - $result['error'] = $e->getMessage(); - } catch (properties\Exception\WrongImageType $e) { - $result['success'] = false; - $result['error'] = $e->getMessage(); + $resource = $this->load($resourceId); } catch (\Exception $e) { - $result['success'] = false; - $result['error'] = $e->getMessage(); + // Resource doesn't exist + $resource = $this->build(); + $this->saveResource($resource); + $resourceId = $resource->id; + } + + $target = $resourceId . '/media/'; + + $path = $this->upload($_FILES['media'], $target); + if ($path != null) { + $media = array('imgUrl' => $path, 'align' => 'right'); + $resource->media = json_encode($media); } - return $result; + $this->saveResource($resource); + return $path; } - public function moveImage($pic, Resource $slide) + public function postThumb(Request $request) { - if ($pic['error'] !== 0) { - throw new \Exception('Upload error'); + $resourceId = intval($request->pullPostVar('slideId')); + try { + $resource = $this->load($resourceId); + } catch (\Exception $e) { + // Resource doesn't exist + //$resource = $this->build(); + //$this->saveResource($resource); + //$resourceId = $resource->id; + return; } - if (!in_array($pic['type'], - array('image/jpeg', 'image/gif', 'image/png'))) { - throw new \properties\Exception\WrongImageType; + $file_string_data = file_get_contents("data://" . $request->pullPostVar('thumb')); + $target = $resourceId . "/thumb/"; + + $path = $this->upload($file_string_data, $target); + if ($path != null) { + $resource->thumb = json_encode($path); } - $dest = $slide->getImagePath(); - if (!is_dir($dest)) { - if (!mkdir($dest, 0755)) { - throw new \Exception('Could not create directory'); - } + $this->saveResource($resource); + return $path; + } + + public function postBackground(Request $request) + { + $resourceId = intval($request->pullPostVar('slideId')); + + try { + $resource = $this->load($resourceId); + } catch (\Exception $e) { + //$resource = $this->build(); + //$this->saveResource($resource); + //$resourceId = $resource->id; + return; } - $file_name = rand() . time() . '.' . \phpws\PHPWS_File::getFileExtension($pic['name']); - $path = $dest . $file_name; - if (!move_uploaded_file($pic['tmp_name'], $path)) { - throw new properties\Exception\FileSaveFailure($path); + $resourcePath = $resourceId . "/background/"; + + $path = $this->upload($_FILES['backgroundMedia'], $resourcePath); + if ($path != null) { + $resource->background = json_encode($path); } + $this->saveResource($resource); return $path; } - public function getCurrentSort($sectionId) + /** + * This function handles the deletion of slides + * @return boolean true if slide was deleted + */ + public function deleteSlide(Request $request) { - $db = Database::getDB(); - $tbl = $db->addTable('ss_slide'); - $tbl->addFieldConditional('sectionId', $sectionId); - $sorting = $tbl->addField('sorting'); - $tbl->addOrderBy('sorting', 'desc'); - $db->setLimit(1); - return $db->selectColumn(); + $resourceId = $request->pullDeleteVar('slideId'); + $resource = $this->load($resourceId); + + $this->deleteSlideDir($resourceId); + + return ($this->deleteResource($resource) != 0); } /** - * Creates an EMPTY slide. Information is added in the PUT. - * @param \slideshow\Factory\Request $request + * Removes all slides associated with a specfic slideId that is within the request + * @param \Canopy\Request request data + * @return boolean true if deletion was successful */ - public function post(Request $request) + public function deleteAll(Request $request) { - $slide = $this->build(); - $slide->sectionId = $request->pullPostInteger('sectionId'); - $slide->content = '

    Content...

    '; - $currentSort = $this->getCurrentSort($slide->sectionId); - if ($currentSort === false) { - $nextSort = 0; - } else { - $nextSort = $currentSort + 1; - } - $slide->sorting = $nextSort; - return $slide; + // Remove all slides comes from Showlist + $vars = $request->getRequestVars(); + $showId = intval($vars['Slide']); + + if (!$this->deleteAllImages($request)) + echo("an error has occured\n"); + + $sql = 'DELETE from ss_slide WHERE showId=:showId;'; + $db = Database::getDB(); + $pdo = $db->getPDO(); + $q = $pdo->prepare($sql); + return $q->execute(array('showId' => $showId)); } - public function save(Resource $slide) + /** + * Deletes an image given a deletion request with slideId + * @return boolean true if deletion was successful + */ + public function deleteImage(Request $request) { - self::saveResource($slide); - return $slide->id; + $resourceId = $request->pullDeleteVar('slideId'); + $resource = $this->load($resourceId); + $media = json_decode($resource->media); + if ($this->removeUpload($resourceId, $media->imgUrl)) { + $resource->media = ""; + $this->saveResource($resource); + return true; + } + return false; } - public function delete($slideId) + /** + * Deletes all images within a specific slideshow given a deletion request with slideId + * @return boolean true if deletion was successful + */ + public function deleteAllImages(Request $request) { - $slide = $this->load($slideId); - self::deleteResource($slide); - $sortable = new \phpws2\Sortable('ss_slide', 'sorting'); - $sortable->startAtZero(); - $sortable->setAnchor('sectionId', $slide->sectionId); - $sortable->reorder(); - $this->deleteImageDirectory($slide); - } + $vars = $request->getRequestVars(); + $showId = intval($vars['Slide']); - public function deleteImageDirectory($slide) - { - $path = $slide->getImagePath(); - \phpws\PHPWS_File::rmdir($path); + $sql = 'SELECT id, media from ss_slide WHERE showId=:showId;'; + $db = Database::getDB(); + $pdo = $db->getPDO(); + $q = $pdo->prepare($sql); + $q->execute(array('showId' => $showId)); + if (!$q) + return false; + $res = $q->fetchAll(); + $flag = false; + foreach ($res as $r) { + $media = json_decode($r['media']); + if ($media != null && !empty($media->imgUrl)) { + $flag = $this->removeUpload($r['id'], $media->imgUrl); + if (!$flag) + echo("an error has occured"); // An error occured + } + $this->deleteSlideDir($r['id']); + } + return true; } - public function createImageDirectory($slide) + private function deleteSlideDir($resourceId) { - $path = $slide->getImagePath(); - if (!is_dir($path)) { - mkdir($path); + $dir = SLIDESHOW_MEDIA_DIRECTORY . $resourceId; + if (is_dir($dir)) { + // If directory exists then we dump it + system('rm -rf ' . escapeshellarg($dir), $ret); + if ($ret != 0) + throw new Exception('Directory Removal Error: ' . $ret); + } else { + echo("directory already removed"); } } - public function clearBackgroundImage($slideId) + /** + * Returns the slide of the show corresponding to the showId + * @var integer showId + * @return array of slides + */ + private function getSlides(int $showId, $includeInactive = false) { - /* @var $slide \slideshow\Resource\SlideResource */ - $slide = $this->load($slideId); - if (is_file($slide->backgroundImage)) { - unlink($slide->backgroundImage); + $sql = 'SELECT * FROM ss_slide WHERE showId=:showId ORDER BY ss_slide.slideIndex;'; + $db = Database::getDB(); + $tbl = $db->addTable('ss_slide'); + $tbl->addFieldConditional('showId', $showId); + if (!$includeInactive) { + $tbl2 = $db->addTable('ss_show', null, false); + $tbl2->addFieldConditional('active', 1); + $tbl->addFieldConditional('showId', $tbl2->getField('id')); } - $slide->backgroundImage = null; - $this->save($slide); + $pdo = $db->getPDO(); + $q = $pdo->prepare($sql); + $q->execute(array('showId' => $showId)); + $slides = $q->fetchAll(); + return $slides; } - public function patch($id, $param, $value) + private function Oldupload($file, $path, $slideId) { - static $allowed_params = array('delay', 'sorting', 'title', 'content', 'backgroundImage'); + # TODO: handle upload of background + # My idea is that I leave the upload process to the path + # and depending on the path, I will upload respectivly + $target = SLIDESHOW_MEDIA_DIRECTORY . $path; + $dir = PHPWS_HOME_DIR . $target; - if (!in_array($param, $allowed_params)) { - throw new \Exception('Parameter may not be patched'); + if (!is_dir($dir)) { + mkdir($dir, 0755, true); } - $slide = $this->load($id); - $slide->$param = $value; - $this->save($slide); - return true; + + if (gettype($file) === 'array') { + $dest = $dir . basename($file['name']); + if (move_uploaded_file($file['tmp_name'], $dest)) { + return './' . $target . basename($file['name']); + } else { + echo("not uploaded and error occured"); + var_dump($file); + var_dump($target); + } + return null; + } else if (gettype($file) === 'string') { // file is a thumbnail or a background img + $dir .= 'thumb/'; + if (is_dir($dir)) { + // If directory exists then we dump it + system('rm -rf ' . escapeshellarg($dir), $ret); + if ($ret != 0) + throw new Exception('Directory Removal Error: ' . $ret); + } + mkdir($dir, 0755, true); + // image will be named based on timestamp + $time = time(); + $filename = $time . '.png'; + $dest = $dir . $filename; + $status = file_put_contents($dest, $file); + if (!$status) { + // error has occured + return null; + } + return './' . $target . 'thumb/' . $filename; + } + return null; } - public function getDecisions(Resource $slide) + /** + * Uploads a file to a given path + * @var mixed - of type array or file string data + * @var string - if path doesn't exist it will recursivly created also it will get dumped if it does exist. + * @return string - filepath of new file + */ + private function upload($file, $path) { - $dFactory = new DecisionFactory; - return $dFactory->listing($slide->id); + $slideshow_path = SLIDESHOW_MEDIA_DIRECTORY . $path; + $canopy_path = PHPWS_HOME_DIR . $slideshow_path; + + if (!is_dir($canopy_path)) { + mkdir($canopy_path, 0755, true); + } else { + system('rm -rf ' . escapeshellarg($canopy_path), $ret); + if ($ret != 0) + throw new Exception('Directory Removal Error: ' . $ret); + mkdir($canopy_path, 0755, true); + } + + if (gettype($file) === 'array') { + $dest = $canopy_path . basename($file['name']); + if (move_uploaded_file($file['tmp_name'], $dest)) { + return './' . $slideshow_path . basename($file['name']); + } // If returns false then error occurred. + echo("not uploaded and error occured"); + //var_dump($file); + var_dump($target); + return null; + } else if (gettype($file) === 'string') { + // We will name the file based on timestamp + $time = time(); + $filename = $time . '.png'; + $dest = $canopy_path . $filename; + //var_dump($dest); + $status = file_put_contents($dest, $file); + if (!$status) { + // error has occured + throw new Exception('Slideshow: File failed to upload'); + return null; + } + return './' . $slideshow_path . $filename; + } } - - public function sort($slide, $new_position) + + private function removeUpload($slideId, $path) { - $sortable = new \phpws2\Sortable('ss_slide', 'sorting'); - $sortable->startAtZero(); - $sortable->setAnchor('sectionId', $slide->sectionId); - $sortable->moveTo($slide->getId(), $new_position); + if (empty($path)) { + return false; + } + try { + unlink($path); + $dir = SLIDESHOW_MEDIA_DIRECTORY . $slideId . '/'; + return true; + } catch (\Exception $e) { + echo("A fatal error has occured: " . $e); + // uncomment to show stacktrace as an array + // var_dump($e); + } + return false; } } diff --git a/class/Resource/BaseResource.php b/class/Resource/BaseAbstract.php similarity index 94% rename from class/Resource/BaseResource.php rename to class/Resource/BaseAbstract.php index 0369574..1c25153 100644 --- a/class/Resource/BaseResource.php +++ b/class/Resource/BaseAbstract.php @@ -9,7 +9,7 @@ use \phpws2\Database; -class BaseResource extends \phpws2\Resource +class BaseAbstract extends \phpws2\Resource { public function __set($name, $value) diff --git a/class/Resource/DecisionResource.php b/class/Resource/DecisionResource.php deleted file mode 100644 index 00c34d4..0000000 --- a/class/Resource/DecisionResource.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * @license http://opensource.org/licenses/lgpl-3.0.html - */ - -namespace slideshow\Resource; - -class DecisionResource extends BaseResource -{ - static $allowedTags = array( - 'strong', 's', 'b', 'a', 'i', 'u', 'ul', 'ol', 'li', 'table', 'tr', - 'td', 'tbody', 'dd', 'dt', 'dl', 'p', 'br', 'div', 'span', 'blockquote', - 'th', 'tt', 'img', 'pre', 'hr', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'fieldset', 'legend', 'code', 'em', 'iframe', 'embed', 'audio', 'video', - 'source', 'object', 'sup', 'sub', 'param', 'strike', 'del', 'abbr', - 'small'); - - /** - * Option listed on the button - * @var \phpws2\Variable\StringVar - */ - protected $title; - - /** - * Message shown when decision made - * @var \phpws2\Variable\StringVar - */ - protected $message; - - /** - * User can continue after picking this decision - * @var \phpws2\Variable\BooleanVar - */ - protected $next; - - /** - * Slide this decision is associated with - * @var \phpws2\Variable\IntegerVar - */ - protected $slideId; - - /** - * Display order of slide - * @var \phpws2\Variable\SmallInteger - */ - protected $sorting; - protected $table = 'ss_decision'; - - public function __construct() - { - parent::__construct(); - $this->title = new \phpws2\Variable\StringVar(null, 'title'); - $this->title->setLimit(50); - $this->message = new \phpws2\Variable\StringVar(null, 'message'); - $this->message->allowEmpty(); - $this->message->addAllowedTags(self::$allowedTags); - $this->next = new \phpws2\Variable\BooleanVar(true, 'next'); - $this->slideId = new \phpws2\Variable\IntegerVar(null, 'slideId'); - $this->sorting = new \phpws2\Variable\SmallInteger(0, 'sorting'); - } - -} diff --git a/class/Resource/QuizResource.php b/class/Resource/QuizResource.php new file mode 100644 index 0000000..9798acd --- /dev/null +++ b/class/Resource/QuizResource.php @@ -0,0 +1,74 @@ +. + * Connor plunkett + * Zack Noble + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +namespace slideshow\Resource; + +class QuizResource extends BaseAbstract +{ + /** + * question of quiz + * @var \phpws2\Variable\StringVar + */ + protected $question; + + /** + * answers for quiz + * @var \phpws2\Variable\ArrayVar + */ + protected $answers; + + /** + * Correct answers for quiz + * @var \phpws2\Variable\ArrayVar + */ + protected $correct; + + /** + * Type of quiz + * @var \phpws2\Variable\StringVar + */ + protected $type; + + /** + * Answer feedback + * @var \phpws2\Variable\ArrayVar + */ + protected $feedback; + + protected $table = 'ss_quiz'; + + public function __construct() + { + parent::__construct(); + $this->question = new \phpws2\Variable\StringVar(null, 'question'); + $this->answers = new \phpws2\Variable\ArrayVar(null, 'answers'); + $this->correct = new \phpws2\Variable\ArrayVar(null, 'correct'); + $this->type = new \phpws2\Variable\StringVar(null, 'type'); + $this->feedback = new \phpws2\Variable\ArrayVar(null, 'feedback'); // ['global' | 'local', $globalCorrect, $globalIncorrect, $localAnswers] + } +} diff --git a/class/Resource/SectionResource.php b/class/Resource/SectionResource.php deleted file mode 100644 index 4b89cde..0000000 --- a/class/Resource/SectionResource.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * @license http://opensource.org/licenses/lgpl-3.0.html - */ - -namespace slideshow\Resource; - -class SectionResource extends BaseResource -{ - - /** - * Show with which this section is associated - * @var \phpws2\Variable\IntegerVar - */ - protected $showId; - - /** - * Title of section - * @var \phpws2\Variable\StringVar - */ - protected $title; - - /** - * Display order of slide - * @var \phpws2\Variable\SmallInteger - */ - protected $sorting; - protected $table = 'ss_section'; - - public function __construct() - { - parent::__construct(); - $this->showId = new \phpws2\Variable\IntegerVar(null, 'showId'); - $this->sorting = new \phpws2\Variable\SmallInteger(1, 'sorting'); - $this->title = new \phpws2\Variable\StringVar(null, 'title'); - $this->title->setLimit('255'); - } - - public function getImagePath() - { - return PHPWS_HOME_DIR . 'images/slideshow/' . $this->id; - } - -} diff --git a/class/Resource/SessionResource.php b/class/Resource/SessionResource.php new file mode 100644 index 0000000..60213cc --- /dev/null +++ b/class/Resource/SessionResource.php @@ -0,0 +1,68 @@ + + * + * @license http://opensource.org/licenses/lgpl-3.0.html + */ + +namespace slideshow\Resource; + +class SessionResource extends BaseAbstract +{ + + /** + * User id + * @var phpws2\Variable\IntegerVar + */ + protected $userId; + + /** + * username + * @var phpws2\Variable\StringVar + */ + protected $username; + + /** + * SlideShow id + * @var phpws2\Variable\IntegerVar + */ + protected $showId; + + /** + * Users highest slide completed + * @var phpws2\Variable\SmallInteger + */ + protected $highestSlide; + + /** + * True if user has completed the slideshow + * @var phpws2\Variable\BooleanVar + */ + protected $completed; + protected $ip; + protected $table = 'ss_session'; + + public function __construct() + { + parent::__construct(); + $this->userId = new \phpws2\Variable\IntegerVar(\Current_User::getId(), 'userId'); + $this->username = new \phpws2\Variable\StringVar(\Current_User::getUsername(), 'username'); + $this->showId = new \phpws2\Variable\IntegerVar(0, 'showId'); + $this->highestSlide = new \phpws2\Variable\SmallInteger(0, 'highestSlide'); + $this->completed = new \phpws2\Variable\BooleanVar(false, 'completed'); + $this->ip = new \phpws2\Variable\Ip(null, 'ip'); + $this->ip->allowNull(true); + } + +} diff --git a/class/Resource/ShowResource.php b/class/Resource/ShowResource.php index 5833197..bd46572 100644 --- a/class/Resource/ShowResource.php +++ b/class/Resource/ShowResource.php @@ -18,7 +18,7 @@ namespace slideshow\Resource; -class ShowResource extends BaseResource +class ShowResource extends BaseAbstract { /** @@ -26,6 +26,37 @@ class ShowResource extends BaseResource * @var \phpws2\Variable\StringVar */ protected $title; + + /** + * + * @var \phpws2\Variable\Boolean + */ + protected $active; + + /** + * + * @var \phpws2\Variable\SmallInteger + */ + protected $slideTimer; + + /** + * Preview image location + * @var \phpws2\Variable\StringVar + */ + protected $preview; + + /** + * Should preview display the first slide image (thumb) + * @var \phpws2\Variable\BooleanVar + */ + protected $useThumb; + + /** + * CSS Animation classname that renders an animation for all slides within the given show + * @var \phpws2\Variable\StringVar + */ + protected $animation; + protected $table = 'ss_show'; public function __construct() @@ -33,6 +64,16 @@ public function __construct() parent::__construct(); $this->title = new \phpws2\Variable\StringVar(null, 'title'); $this->title->setLimit('255'); + $this->active = new \phpws2\Variable\BooleanVar(0, 'active'); + $this->slideTimer = new \phpws2\Variable\SmallInteger(2, 'slideTimer'); + $this->preview = new \phpws2\Variable\StringVar(null, 'preview'); + $this->useThumb = new \phpws2\Variable\BooleanVar(false, 'useThumb'); + $this->animation = new \phpws2\Variable\StringVar('None', 'animation'); + } + + public function getImagePath() + { + return './images/slideshow/' . $this->id . '/'; } } diff --git a/class/Resource/SlideResource.php b/class/Resource/SlideResource.php index ff36c98..221882c 100644 --- a/class/Resource/SlideResource.php +++ b/class/Resource/SlideResource.php @@ -11,77 +11,80 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * - * @author Matthew McNaney + * @author Tyler Craig * * @license http://opensource.org/licenses/lgpl-3.0.html */ namespace slideshow\Resource; -class SlideResource extends BaseResource +class SlideResource extends BaseAbstract { + protected $table = 'ss_slide'; + /** - * Number of seconds until user may click continue - * @var \phpws2\Variable\IntegerVar - */ - protected $delay; + * SlideShow id + * @var phpws2\Variable\IntegerVar + */ + protected $showId; /** - * Id of section to which this slide is associated - * @var \phpws2\Variable\IntegerVar - */ - protected $sectionId; + * Slide index + * Note: think of this as a sequential id for the slides: + * If a slide at index 2 gets deleted, then slide at index 3 will not get decremented + * @var phpws2\Variable\SmallInteger + */ + protected $slideIndex; /** - * Display order of slide - * @var \phpws2\Variable\SmallInteger - */ - protected $sorting; + * Slide Content + * @var phpws2\Variable\StringVar + */ + protected $content; /** - * Title or label of slide. Does not display - * @var \phpws2\Variable\StringVar - */ - protected $title; + * Slide is a quiz + * @var phpws2\Variable\BooleanVar + */ + protected $isQuiz; /** - * Content of the slide. - * @var \phpws2\Variable\StringVar - */ - protected $content; + * Slide background -> Either a color or and image location + * @var phpws2\Variable\StringVar + */ + protected $background; /** - * Prevents navigation. Must use Decisions. - * @var \phpws2\Variable\BooleanVar - */ - protected $locked; + * Media Resource Location + * @var phpws2\Variable\StringVar + */ + protected $media; /** - * - * @var \phpws2\Variable\FileVar - */ - protected $backgroundImage; - protected $table = 'ss_slide'; + * Thumbnail for the slide (img preview of the slideshow content) + * @var phpws2\Variable\StringVar + */ + protected $thumb; + + /** + * quiz's id + * default value is -1 if not quiz + * @var phpws2\Variable\SmallInteger + */ + protected $quizId; public function __construct() { parent::__construct(); - $this->delay = new \phpws2\Variable\IntegerVar(0, 'delay'); - $this->sectionId = new \phpws2\Variable\IntegerVar(null, 'sectionId'); - $this->sorting = new \phpws2\Variable\SmallInteger(0, 'sorting'); - $this->title = new \phpws2\Variable\TextOnly('Untitled slide', 'title'); - $this->title->setLimit('255'); + $this->showId = new \phpws2\Variable\IntegerVar(0, 'showId'); + $this->slideIndex = new \phpws2\Variable\SmallInteger(0, 'slideIndex'); $this->content = new \phpws2\Variable\StringVar(null, 'content'); - $this->locked = new \phpws2\Variable\BooleanVar(false, 'lockout'); - $this->backgroundImage = new \phpws2\Variable\FileVar(null, - 'backgroundImage'); - $this->backgroundImage->allowNull(true); - } - - public function getImagePath() - { - return './images/slideshow/' . $this->sectionId . '/' . $this->id . '/'; + $this->isQuiz = new \phpws2\Variable\BooleanVar(0, 'isQuiz'); + $this->background = new \phpws2\Variable\StringVar('#E5E7E9', 'background'); + $this->media = new \phpws2\Variable\StringVar(null, 'media'); + $this->thumb = new \phpws2\Variable\StringVar(null, 'thumb'); + $this->quizId = new \phpws2\Variable\SmallInteger(0, 'quizId'); } } diff --git a/class/Role/Admin.php b/class/Role/Admin.php index b4a12e7..2d5268c 100644 --- a/class/Role/Admin.php +++ b/class/Role/Admin.php @@ -21,6 +21,8 @@ class Admin extends Base { + protected $name = 'Admin'; + public function isAdmin() { return true; diff --git a/class/Role/Base.php b/class/Role/Base.php index 9e93180..572ccd0 100644 --- a/class/Role/Base.php +++ b/class/Role/Base.php @@ -20,17 +20,19 @@ abstract class Base { + /** * Id of user role. Anonymous users will have id = null * @var integer */ protected $id; + protected $name = 'Base'; - public function __construct($id=null) + public function __construct($id = null) { - $this->id = (int)$id; + $this->id = (int) $id; } - + public function isAdmin() { return false; @@ -45,9 +47,15 @@ public function isLogged() { return false; } - + public function getId() { return $this->id; } + + public function getName() + { + return $this->name; + } + } diff --git a/class/Role/Logged.php b/class/Role/Logged.php index 33990ff..355024f 100644 --- a/class/Role/Logged.php +++ b/class/Role/Logged.php @@ -20,8 +20,12 @@ class Logged extends Base { + + protected $name = 'Logged'; + public function isLogged() { return true; } + } diff --git a/class/Role/User.php b/class/Role/User.php index da492b0..69c0f51 100644 --- a/class/Role/User.php +++ b/class/Role/User.php @@ -20,8 +20,12 @@ class User extends Base { + + protected $name = 'User'; + public function isUser() { return true; } + } diff --git a/class/View/BaseView.php b/class/View/BaseView.php new file mode 100644 index 0000000..4943774 --- /dev/null +++ b/class/View/BaseView.php @@ -0,0 +1,88 @@ + + * @license https://opensource.org/licenses/MIT + */ + +namespace slideshow\View; + +abstract class BaseView +{ + + private function getScript($filename) + { + $root_directory = PHPWS_SOURCE_HTTP . 'mod/slideshow/javascript/'; + if (SLIDESHOW_REACT_DEV) { + $path = "dev/$filename.js"; + } else { + $path = 'build/' . $this->getAssetPath($filename); + } + $script = ""; + return $script; + } + + public function scriptView($view_name, $add_anchor = true, $vars = null) + { + static $vendor_included = false; + if (!$vendor_included) { + $script[] = $this->getScript('vendor'); + $vendor_included = true; + } + if (!empty($vars)) { + $script[] = $this->addScriptVars($vars); + } + $script[] = $this->getScript($view_name); + $react = implode("\n", $script); + if ($add_anchor) { + $content = << +$react +EOF; + return $content; + } else { + return $react; + } + } + + private function addScriptVars($vars) + { + if (empty($vars)) { + return null; + } + foreach ($vars as $key => $value) { + $varList[] = "const $key = '$value';"; + } + return ''; + } + + protected function getRootDirectory() + { + return PHPWS_SOURCE_DIR . 'mod/slideshow/'; + } + + protected function getRootUrl() + { + return PHPWS_SOURCE_HTTP . 'mod/slideshow/'; + } + + private function getAssetPath($scriptName) + { + $rootDirectory = $this->getRootDirectory(); + if (!is_file($rootDirectory . 'assets.json')) { + exit('Missing assets.json file. Run npm run prod in stories directory.'); + } + $jsonRaw = file_get_contents($rootDirectory . 'assets.json'); + $json = json_decode($jsonRaw, true); + if (!isset($json[$scriptName]['js'])) { + throw new \Exception('Script file not found among assets.'); + } + return $json[$scriptName]['js']; + } + +} diff --git a/class/View/SessionView.php b/class/View/SessionView.php new file mode 100644 index 0000000..45632b8 --- /dev/null +++ b/class/View/SessionView.php @@ -0,0 +1,22 @@ + + * @license https://opensource.org/licenses/MIT + */ + + namespace slideshow\View; + + class SessionView extends BaseView + { + + public function sessionTable() + { + return $this->scriptView('session'); + } + + } diff --git a/class/View/ShowView.php b/class/View/ShowView.php new file mode 100644 index 0000000..baa8591 --- /dev/null +++ b/class/View/ShowView.php @@ -0,0 +1,31 @@ + + * @author Tyler Craig + * @license https://opensource.org/licenses/MIT + */ + +namespace slideshow\View; + +use slideshow\Factory\NavBar; + +class ShowView extends BaseView +{ + + public function show() + { + return $this->scriptView('view'); + } + + public function adminShow() + { + return $this->scriptView('shows'); + } + +} diff --git a/class/View/SlideView.php b/class/View/SlideView.php new file mode 100644 index 0000000..f6b12f4 --- /dev/null +++ b/class/View/SlideView.php @@ -0,0 +1,28 @@ + + * @license https://opensource.org/licenses/MIT + */ + +namespace slideshow\View; + +class SlideView extends BaseView +{ + + public function edit() + { + return $this->scriptView('edit'); + } + + public function present() + { + return $this->scriptView('present', true, ['isAdmin' => (int) \Current_User::allow('slideshow')]); + } + +} diff --git a/config/defines.dist.php b/config/defines.dist.php index 619caea..54be8f7 100644 --- a/config/defines.dist.php +++ b/config/defines.dist.php @@ -1,6 +1,6 @@ -
    - - {empty(this.props.checked) ? null : - } -
      -
    {this.props.label}
    - - ) - } -} - -BigCheckbox.propTypes = { - label: PropTypes.string, - checked: PropTypes.oneOfType([PropTypes.bool,PropTypes.string,PropTypes.number]), - handle: PropTypes.func.isRequired -} - -BigCheckbox.defaultProps = { - checked: false -} diff --git a/javascript/AddOn/Form/InputField.jsx b/javascript/AddOn/Form/InputField.jsx deleted file mode 100644 index 1d7ddf6..0000000 --- a/javascript/AddOn/Form/InputField.jsx +++ /dev/null @@ -1,179 +0,0 @@ -'use strict' -import React, {Component} from 'react' -import PropTypes from 'prop-types' - -/** - * When using errorMessage with required, be sure to clear - * the errorMessage prop on successful input - */ - -export default class InputField extends Component { - constructor(props) { - super(props) - - this.state = { - empty: false - } - - this.focus = this.focus.bind(this) - this.handleBlur = this.handleBlur.bind(this) - this.handleChange = this.handleChange.bind(this) - } - - handleBlur(e) { - const value = e.target.value - if (value.length === 0) { - this.setState({empty: true}) - if (this.props.onEmpty) { - this.props.onEmpty() - } - } else { - this.setState({empty: false}) - } - if (this.props.blur) { - this.props.blur() - } - } - - componentDidMount() { - if (this.props.focus === true) { - this.focus() - } - } - - focus() { - this.textInput.focus() - } - - emptyMessage() { - if (this.props.label.length > 0) { - return this.props.label + ' may not be empty' - } else { - return 'Field may not be empty' - } - } - - select(event) { - event.target.select() - } - - handleChange(e) { - const value = e.target.value - if (value.length > 0) { - this.setState({empty: false}) - } - this.props.change(e) - } - - render() { - let inputClass - if ((this.props.errorMessage !== null && this.props.errorMessage !== '') || (this.state.empty && this.props.required && this.props.disableRequireCheck === false)) { - inputClass = 'form-control error-highlight' - } else { - inputClass = 'form-control' - } - let required = this.props.required - ? - : null - - const focus = (input) => { - this.textInput = input - } - - let input = () - - if (this.props.wrap) { - input = this.props.wrap(input) - } - - let errorMessage - if (this.props.errorMessage) { - errorMessage = this.props.errorMessage - } else if (this.state.empty && this.props.required && this.props.disableRequireCheck === false) { - errorMessage = this.emptyMessage() - } - - return ( -
    - {this.props.label.length > 0 - ? - : undefined} - {input} - {errorMessage - ?
    {errorMessage}
    - : null} -
    - ) - } -} - -InputField.defaultProps = { - label: '', - type: 'text', - name: '', - value: '', - change: null, - blur: null, - required: false, - id: null, - autocomplete: false, - placeholder: null, - errorMessage: '', - disabled: false, - size: null, - maxLength: null, - selectOnClick: false, - wrap: null, - onEmpty: null, - flagEmpty: true, - disableRequireCheck: false, - focus : false, - styles: null, -} - -InputField.propTypes = { - name: PropTypes.string, - label: PropTypes.string, - type: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number,]), - change: PropTypes.func.isRequired, - blur: PropTypes.func, - placeholder: PropTypes.string, - errorMessage: PropTypes.string, - iid: PropTypes.string, - autocomplete: PropTypes.bool, - required: PropTypes.bool, - disabled: PropTypes.bool, - size: PropTypes.number, - maxLength: PropTypes.number, - wrap: PropTypes.func, - selectOnClick: PropTypes.bool, - onEmpty: PropTypes.func, - flagEmpty: PropTypes.bool, - disableRequireCheck: PropTypes.bool, - focus: PropTypes.bool, - styles: PropTypes.object, -} - -export const RequiredIcon = () => { - return -} diff --git a/javascript/AddOn/Html/Modal.jsx b/javascript/AddOn/Html/Modal.jsx deleted file mode 100644 index dce8695..0000000 --- a/javascript/AddOn/Html/Modal.jsx +++ /dev/null @@ -1,57 +0,0 @@ -'use strict' -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import ReactModal from 'react-modal' - -export default class Modal extends Component { - constructor(props) { - super(props) - this.state = {} - } - - render() { - const styles = { - overlay: { - backgroundColor: 'rgba(0,0,0,.7)' - }, - content: { - height: this.props.height, - width: this.props.width, - margin: '0 auto', - padding: '0px 3px 3px 3px', - borderRadius: '8px', - }, - } - - const modalContent = { - padding: '6px' - } - return ( - -
    - - - - -
    -
    -
    - {this.props.children} -
    -
    - ) - } -} - -Modal.propTypes = { - children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired, - close: PropTypes.func.isRequired, - width: PropTypes.string, - height: PropTypes.string, - isOpen: PropTypes.bool.isRequired, -} - -Modal.defaultProps = { - width: '400px', - height: '230px', -} diff --git a/javascript/AddOn/Html/Overlay.jsx b/javascript/AddOn/Html/Overlay.jsx deleted file mode 100644 index c398c49..0000000 --- a/javascript/AddOn/Html/Overlay.jsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' -/* global $ */ - -export default class Overlay extends Component { - constructor(props) { - super(props) - this.state = {} - this.lighten = this.lighten.bind(this) - this.normal = this.normal.bind(this) - this.unlockBody = this.unlockBody.bind(this) - this.close = this.close.bind(this) - } - - componentDidMount(){ - this.lockBody() - } - - lockBody() { - $('body').css('overflow', 'hidden') - } - - unlockBody() { - $('body').css('overflow', 'inherit') - } - - normal() { - this.refs.closebutton.style.backgroundColor = 'inherit' - } - - lighten() { - this.refs.closebutton.style.backgroundColor = '#e3e3e3' - } - - close() { - this.unlockBody() - this.props.close() - } - - render() { - const overlayStyle = { - width: '100%', - position: 'fixed', - top: '0px', - bottom: '0px', - left: '0px', - backgroundColor: 'white', - zIndex: '100', - overflowY: 'scroll', - overflowX: 'hidden', - padding: '10px' - } - - const headerStyle = { - backgroundColor: '#F2F2F2', - border: '1px solid #D9D9D9', - marginBottom: '1em', - height: '40px' - } - - const titleStyle = { - padding: '9px', - fontSize: '14px', - fontWeight: 'bold' - } - - const closeButton = { - padding: '4px', - float: 'right' - } - - const childrenStyle = { - paddingBottom : '50px' - } - return ( -
    -
    -
    - -
    -
    {this.props.title}
    -
    -
    - {this.props.children} -
    -
    - ) - } -} - -Overlay.propTypes = { - children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), - close: PropTypes.func.isRequired, - title: PropTypes.string -} diff --git a/javascript/AddOn/Html/Waiting.jsx b/javascript/AddOn/Html/Waiting.jsx deleted file mode 100644 index 9ae783e..0000000 --- a/javascript/AddOn/Html/Waiting.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' - -class Waiting extends Component { - render() { - let message - if (this.props.message.length === 0) { - message = Loading {this.props.label}... - } else { - message = this.props.message - } - return ( -
    -  {message} -
    - ) - } -} - -Waiting.defaultProps = { - label : '' -} - -Waiting.propTypes = { - label: PropTypes.string, - message : PropTypes.string -} - -Waiting.defaultProps = { - message: '', - label: 'data' -} - -export default Waiting diff --git a/javascript/AddOn/Mixin/Abstract.jsx b/javascript/AddOn/Mixin/Abstract.jsx deleted file mode 100644 index 2360121..0000000 --- a/javascript/AddOn/Mixin/Abstract.jsx +++ /dev/null @@ -1,61 +0,0 @@ -'use strict' -import React, {Component} from 'react' -import empty from '../Empty.js' - -/* global $ */ - -export default class Abstract extends Component { - constructor(props) { - super(props) - this.resourceName = null - this.state = { - resource: {}, - } - this.setValue = this.setValue.bind(this) - this.patchValue = this.patchValue.bind(this) - } - - setValue(varname, value) { - if (typeof value === 'object' && value.target !== undefined) { - value = value.target.value - } - let resource = this.state.resource - resource[varname] = value - this.setState({resource}) - } - - patchValue(varname, updateOnEmpty = true) { - if (this.resourceName === null) { - throw 'No resourceName set' - } - if (this.state.resource.id === undefined) { - throw('Resource is missing id') - } - - const resourceValue = this.state.resource[varname] - - if (!updateOnEmpty && empty(resourceValue)) { - return - } - - $.ajax({ - url: `./slideshow/${this.resourceName}/${this.state.resource.id}`, - data: { - varname: varname, - value: resourceValue - }, - dataType: 'json', - type: 'patch', - success: function () {}.bind(this), - error: function () {}.bind(this) - }) - } - - render() { - return ( -
    - ) - } -} - -Abstract.propTypes = {} diff --git a/javascript/Config/Summernote.js b/javascript/Config/Summernote.js deleted file mode 100644 index ceb68e5..0000000 --- a/javascript/Config/Summernote.js +++ /dev/null @@ -1,44 +0,0 @@ -const toolbar = [ - [ - 'style', ['style'], - ], - [ - 'font', - [ - 'bold', 'italic', 'clear', - ], - ], - [ - 'color', ['color'], - ], - [ - 'fontname', ['fontsize'], - ], - [ - 'para', - [ - 'ul', 'ol', 'paragraph', - ], - ], - [ - 'table', ['table'], - ], - [ - 'insert', - [ - 'link', 'picture', 'video', - ], - ], - [ - 'view', - [ - 'fullscreen', 'codeview', - ], - ], -] - -export const options = { - minHeight: 400, - dialogsInBody: true, - toolbar: toolbar, -} diff --git a/javascript/DecisionButtons.js b/javascript/DecisionButtons.js deleted file mode 100644 index 0a20bf1..0000000 --- a/javascript/DecisionButtons.js +++ /dev/null @@ -1,13 +0,0 @@ -const makeActive = function() { - let slideTimeout = setTimeout(function(){ - $('.controls').addClass('active') - window.clearTimeout(slideTimeout) - }, 3000) -} - -makeActive() - -Reveal.addEventListener('slidechanged', function(event){ - $('.controls').removeClass('active') - makeActive(); -}) diff --git a/javascript/Decisions/Decisions.jsx b/javascript/Decisions/Decisions.jsx deleted file mode 100644 index 637396f..0000000 --- a/javascript/Decisions/Decisions.jsx +++ /dev/null @@ -1,18 +0,0 @@ -'use strict' -import React, {Component} from 'react' -import PropTypes from 'prop-types' - -export default class Decisions extends Component { - constructor(props) { - super(props) - this.state = {} - } - - render() { - return ( -
    - ) - } -} - -Decisions.propTypes = {} diff --git a/javascript/Edit/AddOn/AnimateDropdown.jsx b/javascript/Edit/AddOn/AnimateDropdown.jsx new file mode 100644 index 0000000..e0da052 --- /dev/null +++ b/javascript/Edit/AddOn/AnimateDropdown.jsx @@ -0,0 +1,33 @@ +import React, {useState, useEffect} from 'react' +import {Dropdown} from 'react-bootstrap' + +export default function AnimateDropdown(props) { + function setAnimation(a) { + $.ajax({ + url: './slideshow/Show/' + props.id, + type: 'PATCH', + dataType: 'json', + data: {animation: a}, + success: () => console.log("animation changed to ", a), + error: (req, res) => console.log(res) + }) + props.setAnimation(a) + } + + return ( + + + {props.animation} + + + + setAnimation('None')}>None + setAnimation('slideInRight')}>slideInRight + setAnimation('slideInLeft')}>slideInLeft + setAnimation('bounceInRight')}>bounceInRight + setAnimation('bounceInLeft')}>bounceInLeft + setAnimation('zoomIn')}>zoomIn + + + ) +} \ No newline at end of file diff --git a/javascript/Edit/AddOn/ColorSelect.jsx b/javascript/Edit/AddOn/ColorSelect.jsx new file mode 100644 index 0000000..e78076d --- /dev/null +++ b/javascript/Edit/AddOn/ColorSelect.jsx @@ -0,0 +1,59 @@ +import React, {useState} from 'react' +import {Row, Col} from 'react-bootstrap' +import Tippy from '@tippyjs/react' +import {CirclePicker, SketchPicker} from 'react-color' + +export default function ColorSelect(props) { + const [pickerColor, setPickerColor] = useState(false) // Allows for the picker to maintain the current color + + function handleColorChange(color) { + setPickerColor(color.hex) + props.changeBackground(color.hex) + } + + return ( + + +
    Color
    + + + + + + + } + arrow + interactive> + + + +
    + ) +} diff --git a/javascript/Edit/AddOn/ImageColumn.jsx b/javascript/Edit/AddOn/ImageColumn.jsx new file mode 100644 index 0000000..d29a6bf --- /dev/null +++ b/javascript/Edit/AddOn/ImageColumn.jsx @@ -0,0 +1,71 @@ +'use strict' +import React, { Component } from 'react' +import Tippy from '@tippyjs/react' +import 'tippy.js/themes/light-border.css' +import PropTypes from 'prop-types' + +export default class ImageColumn extends Component { + render() { + const left = ( + + Align Left + + ) + const right = ( + + Align Right + + ) + + const settingsButtons = ( +
    + + +
    + ) + + const imgStyle = { + height: this.props.height, + width: this.props.width, + bjectFit: 'scale-down', + } + + return ( +
    + + {this.props.src} + + +
    + ) + } +} + +ImageColumn.propTypes = { + remove: PropTypes.func, + align: PropTypes.func, + mediaAlign: PropTypes.string, + height: PropTypes.string, + width: PropTypes.string, + src: PropTypes.string, +} diff --git a/javascript/Edit/DraftEditor.jsx b/javascript/Edit/DraftEditor.jsx new file mode 100644 index 0000000..ce1ddc9 --- /dev/null +++ b/javascript/Edit/DraftEditor.jsx @@ -0,0 +1,199 @@ +import React, {useEffect, useState} from 'react' +import { + Editor, + EditorState, + ContentState, + RichUtils, + KeyBindingUtil, + convertToRaw, + convertFromRaw, +} from 'draft-js' +import CustomBlockFn from '../Resources/Draft/CustomBlockFn' +import CustomStyleMap from '../Resources/Draft/CustomStyleMap' +import decorator from '../Resources/Draft/LinkDecorator' + +import Toolbar from './Toolbar/Toolbar' +import ImageC from './AddOn/ImageColumn' +import PropTypes from 'prop-types' + +//const {hasCommandModifier} = KeyBindingUtil + +/** Props: + * readOnly : boolean // Ensures the editor is non-ediable and the toolbar doesn't appear + * content : Object + * currentSlide : number + * saveContentState : function // saves extracted contentState and appends it to content : Object + */ + +export default function DraftEditor(props) { + const [editorState, setEditorState] = useState( + EditorState.createEmpty(decorator) + ) + + useEffect(() => { + loadEditorState() + }, [props.currentSlide]) + + useEffect(() => { + saveEditorState(editorState) + }, [editorState]) + + function loadEditorState() { + // If we aren't loading a quiz + // If there isn't any content then we make some + if (props.content.saveContent == undefined) { + const eState = EditorState.createWithContent( + ContentState.createFromText('New Slide'), + decorator + ) + // Set inital block type to H1 + setEditorState(RichUtils.toggleBlockType(eState, 'header-one')) + } else { + if (props.content.saveContent.length > 0) { + const contentState = convertFromRaw( + JSON.parse(props.content.saveContent) + ) + setEditorState(EditorState.createWithContent(contentState, decorator)) + } else { + // This means that content is defined but there is no data to be read + console.log( + 'An error has occured. Your data may have been corrupted. This slide will be reset.' + ) + const eState = EditorState.createWithContent( + ContentState.createFromText('New Slide'), + decorator + ) + setEditorState(RichUtils.toggleBlockType(eState, 'header-one')) + } + } + } + + function saveEditorState() { + if (editorState != undefined && !props.readOnly) { + // See draft.js documentation to understand what these are: + let contentState = editorState.getCurrentContent() + let saveContent = JSON.stringify(convertToRaw(contentState)) + + props.saveContentState(saveContent) + } + } + + // Previously commented code can be read here: + //https://github.com/AppStateESS/slideshow/blob/549affb8e45a498a293d7a8ab04dd3983ef9f965/javascript/Edit/DraftEditor.jsx#L62 + + /* Custom styles mappings for Draft Editor Begin */ + let styles = CustomStyleMap + if (editorState != undefined) { + // This adds custom styles to the mix + editorState.getCurrentInlineStyle().map((customStyle) => { + if (customStyle != undefined) { + let styleObj = undefined + // Custom text color + if (customStyle.charAt(0) === '#') { + styleObj = JSON.parse( + '{"' + customStyle + '":{"color":"' + customStyle + '"}}' + ) + } // Custom font sizes would be added as an else if here + if (styleObj != undefined) { + styles = Object.assign(styleObj, CustomStyleMap) + } + } + }) + } + //background styling check image or color + let backgroundStyle = { + minHeight: 500, + minWidth: 300, + height: '8rem', + overflow: 'auto', + backgroundImage: `url(${props.content.background})`, + backgroundRepeat: 'no-repeat', + backgroundSize: 'cover', + } + if (props.content.background.charAt(0) === '#') { + backgroundStyle = { + minHeight: 500, + minWidth: 300, + height: '8rem', + overflow: 'auto', + backgroundColor: props.content.background, + } + } + + /* Image Column Initalization */ + let imgC = undefined + let imgAlign = undefined + if (props.content.media != undefined) { + imgC = ( + props.saveMedia(props.content.media.imgUrl, a)} + mediaAlign={props.content.media.align} + height={'100%'} + width={'100%'} + /> + ) + imgAlign = props.content.media.align + } + + return ( +
    + {props.readOnly ? undefined : ( + setEditorState(eState)} + getEditorState={() => editorState} + saveMedia={props.saveMedia} + saveBackground={props.saveBackground} + insertMedia={props.insertMedia} + validate={props.validate} + mediaView={props.mediaView} + mediaCancel={props.mediaCancel} + mediaOpen={props.mediaOpen} + /> + )} +

    +
    +
    + {imgAlign === 'left' ? imgC : undefined} +
    +
    + this.setState({ hasFocus: true })} + //onBlur={() => this.setState({ hasFocus: false })} + customStyleMap={styles} + blockStyleFn={CustomBlockFn} + spellCheck={true} + readOnly={props.readOnly} + /> +
    +
    + {imgAlign === 'right' ? imgC : undefined} +
    +
    +
    + ) +} + +DraftEditor.propTypes = { + currentSlide: PropTypes.number, + content: PropTypes.object, + readOnly: PropTypes.bool, + saveContentState: PropTypes.func, + removeMedia: PropTypes.func, + saveMedia: PropTypes.func, + saveBackground: PropTypes.func, + validate: PropTypes.func, + mediaView: PropTypes.bool, + mediaCancel: PropTypes.func, + mediaOpen: PropTypes.func +} diff --git a/javascript/Edit/Edit.jsx b/javascript/Edit/Edit.jsx new file mode 100644 index 0000000..3701302 --- /dev/null +++ b/javascript/Edit/Edit.jsx @@ -0,0 +1,464 @@ +'use strict' +import React, {Component} from 'react' +import EditView from './EditView.jsx' +import NavBar from './NavBar.jsx' +import NavCards from './NavCards.jsx' +import {Button, InputGroup, FormControl} from 'react-bootstrap' +import {getPageId} from '../api/getPageId' +import {fetchQuiz, postQuiz} from '../api/quiz' +import domtoimage from '../Resources/dom-to-image' +import Skeleton from '../Resources/Components/Skeleton.jsx' + +/* global $ */ + +export default class Edit extends Component { + constructor() { + super() + + let showId = getPageId() + + this.state = { + currentSlide: 0, + id: showId, + showTitle: 'Edit:', + editTitleView: false, + content: [ + { + saveContent: undefined, + quizContent: undefined, + isQuiz: false, + background: '#E5E7E9', + media: {imgUrl: '', align: ''}, + slideId: 0, + thumb: undefined, + }, + ], + slideTimer: 2, + loaded: false, + animation: 'None', + } + + this.save = this.save.bind(this) + this.load = this.load.bind(this) + this.loadQuiz = fetchQuiz.bind(this) + this.saveDomScreen = this.saveDomScreen.bind(this) + this.setCurrentSlide = this.setCurrentSlide.bind(this) + this.addNewSlide = this.addNewSlide.bind(this) + this.addNewQuiz = this.addNewQuiz.bind(this) + this.pushNewSlide = this.pushNewSlide.bind(this) + this.deleteCurrentSlide = this.deleteCurrentSlide.bind(this) + this.moveSlide = this.moveSlide.bind(this) + this.saveTitle = this.saveTitle.bind(this) + this.saveContentState = this.saveContentState.bind(this) + this.saveQuizContent = this.saveQuizContent.bind(this) + this.saveMedia = this.saveMedia.bind(this) + this.removeMedia = this.removeMedia.bind(this) + this.saveBackground = this.saveBackground.bind(this) + } + + componentDidMount() { + this.load() + } + + async save() { + this.saveDomScreen() + await $.ajax({ + url: './slideshow/Slide/' + window.sessionStorage.getItem('id'), + data: { + slides: [...this.state.content], + }, + type: 'put', + dataType: 'json', + success: (slideIds) => { + // Update slideIds + let c = [...this.state.content] + slideIds.map((id, i) => { + c[i].slideId = id + }) + this.setState({content: c}, () => { + window.sessionStorage.setItem( + 'slideId', + this.state.content[this.state.currentSlide].slideId + ) + }) + }, + error: (req, err) => { + alert('Failed to save show ' + window.sessionStorage.getItem('id')) + document.write(req.responseJSON.backtrace[0].args[1].xdebug_message) + console.error(req, err.toString()) + }, + }) + } + + async load() { + $.ajax({ + url: './slideshow/Slide/edit/?id=' + window.sessionStorage.getItem('id'), + type: 'GET', + dataType: 'json', + success: async function (data) { + let loaded = data['slides'] + if (loaded[0] != undefined) { + window.sessionStorage.setItem('slideId', loaded[0].id) + let showContent = [] + for (let i = 0; i < loaded.length; i++) { + let saveC = undefined + let quizC = null + let qId = -1 + // We may not need a quizConv anymore... + let isQ = this.quizConv(loaded[i].isQuiz) + if (!isQ) { + saveC = loaded[i].content + } else { + qId = Number(loaded[i].quizId) + quizC = await this.loadQuiz(qId) + } + showContent.push({ + isQuiz: isQ, + saveContent: saveC, + quizContent: quizC, + background: loaded[i].background, + thumb: JSON.parse(loaded[i].thumb || '{}'), // Ensure that this isn't undefined + slideId: Number(loaded[i].id), + media: JSON.parse(loaded[i].media || '{}'), + quizId: qId, + }) + } + this.setState({ + content: showContent, + id: loaded[0].showId, + loaded: true, + }) + } else { + this.save() + this.setState({loaded: true}) + } + }.bind(this), + error: function (req, err) { + alert('Failed to load data.') + console.error(req, err.toString()) + }.bind(this), + }) + + $.ajax({ + url: + './slideshow/Show/present/?id=' + window.sessionStorage.getItem('id'), + type: 'GET', + dataType: 'json', + success: (data) => { + this.setState({ + slideTimer: Number(data[0].slideTimer), + showTitle: data[0].title, + animation: data[0].animation, + }) + }, + error: (request, response) => { + console.log(request) + console.error(response) + }, + }) + } + + saveDomScreen(domNode, index) { + if (domNode === undefined) { + domNode = document.getElementById('editor') + } + if (domNode != null) { + index = domNode.getAttribute('data-key') + } else { + return + } + if (domNode.getAttribute('data-key') == index) { + domtoimage + .toPng(domNode) + .then((dataUrl) => { + let img = new Image() + img.src = dataUrl + img.width = 200 + img.height = 100 + let fData = new FormData() + fData.append('thumb', img.src) + fData.append('slideId', this.state.content[index].slideId) + $.post({ + url: + './slideshow/Slide/thumb/' + window.sessionStorage.getItem('id'), + type: 'POST', + data: fData, + processData: false, + contentType: false, + success: (path) => { + let c = [...this.state.content] + c[index].thumb = JSON.parse(path) + this.setState({content: c}) + }, + error: (req, res) => { + console.log(req) + console.error(res) + }, + }) + }) + .catch(function (error) { + console.error(error) + }) + } + } + + setCurrentSlide(val) { + this.setState({currentSlide: val}, () => this.save()) + } + + addNewSlide(quizId) { + if (typeof quizId != 'number') quizId = -1 // an event is bindinded on some calls which causes errors + /* This function adds to the stack of slides held within state.content */ + const index = this.state.currentSlide + 1 + const newSlide = { + saveContent: undefined, + quizContent: undefined, + isQuiz: quizId !== -1, + background: '#E5E7E9', + quizId: quizId, + } + let copy = [...this.state.content] + copy.splice(index, 0, newSlide) + this.setState( + { + //currentSlide: index, + content: copy, + }, + () => { + this.save() + this.setCurrentSlide(index) + } + ) + } + + async addNewQuiz() { + const id = await postQuiz() + sessionStorage.setItem('quizId', id) + this.addNewSlide(id) + } + + // addNewSlide to the end + pushNewSlide() { + const index = this.state.content.length + const newSlide = { + saveContent: undefined, + quizContent: undefined, + isQuiz: false, + background: '#E5E7E9', + } + let copy = [...this.state.content] + copy.push(newSlide) + this.setState( + { + //currentSlide: index, + content: copy, + }, + () => this.setCurrentSlide(index) + ) + } + + deleteCurrentSlide() { + let copy = [...this.state.content] + let isQuiz = copy[this.state.currentSlide].isQuiz + if (isQuiz) { + let quizId = copy[this.state.currentSlide].quizId + $.ajax({ + url: `./slideshow/Quiz/${quizId}`, + type: 'delete', + success: () => { + console.log('quiz deleted!!!') + }, + error: (req, res) => { + console.error(res) + }, + }) + } + let newIndex = this.state.currentSlide + // splice one slide at the current index + copy.splice(this.state.currentSlide, 1) + // Current slide is the first slide and there are no other slides + if (this.state.currentSlide === 0 && this.state.content.length == 1) { + // set the array to an empty slide + copy = [{saveContent: undefined, isQuiz: false, background: '#E5E7E9'}] + } + // If we are deleting the last slide + if (this.state.currentSlide == copy.length) { + newIndex = this.state.currentSlide - 1 + } + + $.ajax({ + url: './slideshow/Slide/' + window.sessionStorage.getItem('id'), + type: 'delete', + data: { + slideId: this.state.content[this.state.currentSlide].slideId, + type: 'slide', + }, + error: (req, res) => { + console.error(req, res.toString()) + }, + }) + + this.setState( + { + content: copy, + currentSlide: newIndex, + } /*() => this.setCurrentSlide(newIndex)*/ + ) + } + + moveSlide(fromIndex, toIndex) { + let c = [...this.state.content] + let slide = c[fromIndex] + c.splice(fromIndex, 1) + c.splice(toIndex, 0, slide) + this.setState({content: c}) + } + + saveTitle() { + $.ajax({ + url: './slideshow/Show/' + window.sessionStorage.getItem('id'), + data: {title: this.state.showTitle}, + type: 'put', + dataType: 'json', + success: function () { + this.setState({editTitleView: false}) + }.bind(this), + error: function (req, err) { + alert('Failed to save data.') + console.error(req, err.toString()) + }.bind(this), + }) + } + + saveContentState(saveContent) { + if (saveContent != undefined) { + let c = [...this.state.content] + c[this.state.currentSlide].saveContent = saveContent + this.setState({content: c}) + } + } + + saveQuizContent(quizContent) { + let c = [...this.state.content] + c[this.state.currentSlide].quizContent = quizContent + this.setState({content: c}) + } + + saveMedia(imgUrl, align) { + let c = [...this.state.content] + c[this.state.currentSlide].media = {imgUrl: imgUrl, align: align} + this.setState({content: c}, () => this.save()) + } + + removeMedia() { + $.ajax({ + url: './slideshow/Slide/' + window.sessionStorage.getItem('id'), + method: 'delete', + data: { + type: 'image', + slideId: this.state.content[this.state.currentSlide].slideId, + }, + error: (req, res) => { + console.log(res.toString()) + }, + }) + let c = [...this.state.content] + c[this.state.currentSlide].media = '' + this.setState({content: c}) + } + + saveBackground(newBackground) { + let c = [...this.state.content] + c[this.state.currentSlide].background = newBackground + this.setState({content: c}, () => this.save()) + } + + quizConv(quizT) { + // When we load from the data base the isQuiz boolean + // is loaded in as a string or a number + // We need to handle that and bring it back to a boolean + if (quizT == undefined) return false // initial load + if (typeof JSON.parse(quizT) === 'number') return JSON.parse(quizT) != 0 + return typeof JSON.parse(quizT) === 'boolean' ? quizT : JSON.parse(quizT) + } + + render() { + if (!this.state.loaded) return + // below never used + //let isQuiz = this.state.content[this.state.currentSlide].isQuiz + let cardTitle + if (this.state.editTitleView) { + cardTitle = ( + + this.setState({showTitle: event.target.value})} + onKeyDown={(event) => { + if (event.key === 'Enter') this.saveTitle() + }} + /> + + + + + ) + } else { + cardTitle = ( + + ) + } + + return ( +
    +

    {cardTitle}

    + this.setState({animation: a})} + /> +
    + + +
    +
    + ) + } +} diff --git a/javascript/Edit/EditView.jsx b/javascript/Edit/EditView.jsx new file mode 100644 index 0000000..ff508f2 --- /dev/null +++ b/javascript/Edit/EditView.jsx @@ -0,0 +1,199 @@ +import React, { useState } from 'react' +import PropTypes from 'prop-types' + +import QuizEdit from './Quiz/QuizEdit.jsx' +import QuizView from './Quiz/QuizView.jsx' +import Editor from './DraftEditor' +import ToolbarQ from './Toolbar/QuizToolbar.jsx' +import { Modal } from 'react-bootstrap' +import Dropzone from 'react-dropzone-uploader' +import ImageC from './AddOn/ImageColumn' + + +const EditView = ({ + content, + currentSlide, + saveContentState, + saveMedia, + removeMedia, + saveBackground, + saveQuizContent, + load, +}) => { + const [editView, setEditView] = useState(false) + const [mediaView, setMediaView] = useState(false) + //const [mediaAlign, setMediaAlign] = useState('right') + const mediaAlign = 'right' + + + function mediaOpen() { + setMediaView(true) + } + + function mediaCancel() { + setMediaView(false) + } + + function validate({ meta }) { + if (meta.status === 'rejected_file_type') { + alert('Sorry, this file type is not supported') + } + } + + function insertMedia(fileWithMeta) { + let showId = Number(window.sessionStorage.getItem('id')) + let slideId = Number(window.sessionStorage.getItem('slideId')) + // Handle AJAX + let fMeta = fileWithMeta[0] + let formData = new FormData() + formData.append('media', fMeta.file) + formData.append('slideId', slideId) + formData.append('id', showId) + + $.ajax({ + url: './slideshow/Slide/image/' + window.sessionStorage.getItem('id'), + type: 'post', + data: formData, + processData: false, + contentType: false, + success: (imageUrl) => { + saveMedia(JSON.parse(imageUrl), 'right') + if (content.isQuiz) { + alert("Image has been Saved") + } + }, + error: (req, res) => { + console.log(req) + console.error(res) + alert( + 'An error has occured with this image. Please try a different image.' + ) + }, + }) + setMediaView(false) + } + + + let imgC = undefined + let imgAlign = undefined + if (content.media != undefined) { + imgC = ( + saveMedia(content.media.imgUrl, a)} + mediaAlign={content.media.align} + height={'100%'} + width={'100%'} + /> + ) + imgAlign = content.media.align + } + + let backgroundStyle = { + minHeight: 500, + minWidth: 300, + height: '8rem', + overflow: 'auto', + backgroundImage: `url(${content.background})`, + backgroundRepeat: 'no-repeat', + backgroundSize: 'cover', + } + if (content.background.charAt(0) === '#') { + backgroundStyle = { + minHeight: 500, + minWidth: 300, + height: '8rem', + overflow: 'auto', + backgroundColor: content.background, + } + } + + if (!content.isQuiz) { + return ( + + ) + } + if (editView || content.quizContent == null) { + return ( +
    + setEditView(!editView)} + view={editView} + /> + + setEditView(false)} + load={load} + validate={validate} + insertMedia={insertMedia} + changeBackground={saveBackground} + mediaView={mediaView} + mediaCancel={mediaCancel} + mediaOpen={mediaOpen} + /> +
    + ) + } + return ( +
    +
    + setEditView(!editView)} + view={editView} + /> + +
    +
    + {imgAlign === 'left' ? imgC : undefined} +
    + setEditView(true)} + /> +
    + {imgAlign === 'right' ? imgC : undefined} +
    +
    +
    +
    + ) +} + +EditView.propTypes = { + content: PropTypes.object, + currentSlide: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + saveMedia: PropTypes.func, + removeMedia: PropTypes.func, + saveContentState: PropTypes.func, + saveBackground: PropTypes.func, + saveQuizContent: PropTypes.func, + load: PropTypes.func, + insertMedia: PropTypes.func, + validate: PropTypes.func, + mediaView: PropTypes.bool, + mediaCancel: PropTypes.func, + mediaOpen: PropTypes.func +} + +export default EditView diff --git a/javascript/Edit/NavBar.jsx b/javascript/Edit/NavBar.jsx new file mode 100644 index 0000000..7353c45 --- /dev/null +++ b/javascript/Edit/NavBar.jsx @@ -0,0 +1,97 @@ +'use strict' +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import { + Button, + ButtonGroup, + ButtonToolbar, + Dropdown, + DropdownButton, +} from 'react-bootstrap' + +import Settings from './Settings.jsx' + +export default class NavBar extends Component { + constructor() { + super() + + this.returnToShowList = this.returnToShowList.bind(this) + this.handlePresent = this.handlePresent.bind(this) + } + + async returnToShowList() { + await this.props.saveDB() + window.location.href = './slideshow/Show/list' + } + + async handlePresent() { + if (this.props.id == -1) { + alert( + "A problem has occurred with your browser's session. This is most likely caused by an attempt to present an empty show." + ) + //window.location.href = './slideshow/Show/list' + } else { + await this.props.saveDB() + window.sessionStorage.setItem('id', this.props.id) + window.location.href = './slideshow/Slide/Present/?id=' + this.props.id + } + } + + render() { + return ( +
    + + + + + + + + + Insert Slide + + + Insert Quiz + + + + Delete Slide + + + + + + +
    + ) + } +} + +NavBar.propTypes = { + insertSlide: PropTypes.func, + insertQuiz: PropTypes.func, + deleteSlide: PropTypes.func, + currentSlide: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + saveDB: PropTypes.func, + id: PropTypes.number, + saveBackground: PropTypes.func, + currentColor: PropTypes.string, + slideTimer: PropTypes.number, +} diff --git a/javascript/Edit/NavCards.jsx b/javascript/Edit/NavCards.jsx new file mode 100644 index 0000000..488fd8a --- /dev/null +++ b/javascript/Edit/NavCards.jsx @@ -0,0 +1,186 @@ +'use strict' +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import './' + +export default class NavCards extends Component { + constructor(props) { + super(props) + this.state = { + currentSlide: this.props.currentSlide, + dragItem: -1, + dragLineIndex: -1, + addSlideHover: false, + } + this.handleSlide = this.handleSlide.bind(this) + this.handleNewSlide = this.handleNewSlide.bind(this) + } + + componentDidMount() { + document.addEventListener( + 'dragenter', + (event) => { + if ( + event.target.className === 'thumb' && + Number(event.target.id) >= 0 + ) { + this.setState({dragLineIndex: Number(event.target.id)}) + } + }, + false + ) + + document.addEventListener( + 'dragstart', + (event) => { + let img = event.target.cloneNode(true) + // TODO: insert custom image or logo + // right now i pass the slide and make it render off screen + event.dataTransfer.setDragImage(img, -5000, 100) + this.setState({ + dragLineIndex: this.props.currentSlide, + dragItem: event.target.id, + }) + }, + false + ) + + document.addEventListener( + 'dragend', + (event) => { + //event.target.style.display = "initial" + this.props.moveSlide(this.state.dragItem, this.state.dragLineIndex) + this.props.setCurrentSlide(this.state.dragLineIndex) + this.setState({dragLineIndex: -1}) + }, + false + ) + } + + componentWillUnmount() { + document.removeEventListener('dragenter') + document.removeEventListener('dragstart') + document.removeEventListener('dragend') + } + + handleSlide(event) { + this.props.setCurrentSlide(event.target.value - 1) + } + + handleNewSlide() { + this.props.addNewSlide() + } + + render() { + let data = this.props.content.map((slide, i) => { + let key = + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15) //#1E90FF + let highlight = + this.props.currentSlide == i + ? {border: 'solid 3px #337ab7', borderRadius: 3, zIndex: -1} + : {padding: '3px'} + let top = this.state.dragItem >= this.state.dragLineIndex + let bar = + this.state.dragLineIndex != -1 && i == this.state.dragLineIndex + ? {border: 'solid 1px #337ab7'} + : {border: 'solid 1px white'} + const imgSrc = + typeof slide.thumb == 'string' + ? slide.thumb + : 'mod/slideshow/img/loading_placeholder.png' + return ( + + {top ?
    : undefined} +
    this.props.setCurrentSlide(i)} + key={i}> + {'loading...'} +
    + {!top ?
    : undefined} +
    + ) + }) + + return ( +
    this.props.saveDomScreen()}> + {data} +
    this.setState({addSlideHover: true})} + onMouseLeave={() => this.setState({addSlideHover: false})}> + Add New Slide +

    + +
    +
    + ) + } +} + +const cardStyle = { + width: 175, + height: 100, + marginBottom: 5, + marginTop: 5, +} + +const addSlideStyle = { + border: 'dashed 1px ', + width: 175, + height: 100, + textAlign: 'center', + justifyContent: 'center', + color: '#337ab7', + marginBottom: 20, + marginTop: 10, +} + +const addSlideHover = { + border: 'solid 3px', + color: '007bff', + width: 175, + height: 100, + textAlign: 'center', + justifyContent: 'center', + marginBottom: 20, + marginTop: 10, + cursor: 'pointer', +} + +const containerStyle = { + overflowY: 'scroll', + height: 600, + maxWidth: 210, + marginTop: 75, + scrollbarWidth: 'thin', + marginBottom: 10, +} + +NavCards.propTypes = { + slides: PropTypes.array, + currentSlide: PropTypes.number, + setCurrentSlide: PropTypes.func, + addNewSlide: PropTypes.func, + moveSlide: PropTypes.func, +} diff --git a/javascript/Edit/OldEditView.jsx b/javascript/Edit/OldEditView.jsx new file mode 100644 index 0000000..e85358d --- /dev/null +++ b/javascript/Edit/OldEditView.jsx @@ -0,0 +1,139 @@ +'use strict' +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +import QuizEdit from './Quiz/QuizEdit.jsx' +import QuizView from './Quiz/QuizView.jsx' + +import Editor from './DraftEditor' +import ToolbarC from './Toolbar/Toolbar.jsx' +import ToolbarQ from './Toolbar/QuizToolbar.jsx' +import ImageC from './AddOn/ImageColumn.jsx' + + + +import 'animate.css' + +export default class EditView extends Component { + + constructor(props) { + super(props) + this.state = { + quizEditView: true, + imgUrl: props.content.media.imgUrl, + mediaAlign: '' + } + + this.saveQuizContent = this.saveQuizContent.bind(this) + this.toggleQuizEdit = this.toggleQuizEdit.bind(this) + this.alignMedia = this.alignMedia.bind(this) + } + + componentDidUpdate(prevProps, prevState) { + + // handle of non-quiz slides + if (this.props.content != undefined && !this.props.content.isQuiz) { + if (this.props.currentSlide != prevProps.currentSlide || this.props.content.saveContent != prevProps.content.saveContent) { + // catch the load from the database and the changing of slides + if (this.props.content.media != undefined) { + this.setState({imgUrl: this.props.content.media.imgUrl, mediaAlign: this.props.content.media.align}) + } + this.loadEditorState() + } + else if (this.state.updated) { + // current component updated + this.saveEditorState() + this.setState({ updated: false }) + } + } + + // If quiz component updated and the data is there then we switch to view mode else we switch to edit mode. + if (prevProps.content.quizContent != this.props.content.quizContent) { + // Yes i understand this would be a good spot to use a ternary operator, but we use that too much. :P + if (this.props.content.quizContent != null /*&& this.props.content.quizContent != null*/) { + this.setState({ quizEditView: false }) + } + else { + this.setState({ quizEditView: true }) + } + } + + let newImgUrl = (this.props.content.media == undefined) ? "" : this.props.content.media.imgUrl + let newAlign = (this.props.content.media == undefined) ? "" : this.props.content.media.align + if (this.state.imgUrl != newImgUrl) { + this.setState({imgUrl: newImgUrl, mediaAlign: newAlign}) + } + } + + + + saveQuizContent(quizContent) { + this.toggleQuizEdit() + this.props.saveQuizContent(quizContent) + } + + async toggleQuizEdit() { + this.setState({ + quizEditView: !this.state.quizEditView + }) + } + + alignMedia() { + let align = (this.state.mediaAlign === 'right') ? 'left' : 'right' + this.setState({mediaAlign: align}) + this.props.saveMedia(this.state.imgUrl, align) + } + + render() { + + + let quizView = (this.state.quizEditView) ? + : + + + let editRender = (this.props.content.isQuiz) ? (quizView) : () // TODO + let imgRender = (this.state.imgUrl != undefined) ? + // Note: we can custom the width and length through these fields + : undefined + let toolbar = (this.props.content.isQuiz) ? + : + this.state.editorState} saveMedia={this.props.saveMedia}/> + + return ( +
    +

    +
    + {toolbar} +
    + { + // This code is a little-let's not kid ourselves, it's quite a bit messy. Basicly, I need a different view for quizEdit then the other three views + (this.state.quizEditView && this.props.content.isQuiz) ? quizView : + ( +
    +
    + {(this.state.mediaAlign === 'left') ? imgRender : undefined} +
    + {editRender} +
    + {(this.state.mediaAlign === 'right') ? imgRender : undefined} +
    +
    ) + } +
    +
    + ) + } +} + + +EditView.propTypes = { + currentSlide: PropTypes.number, + content: PropTypes.object, + saveContentState: PropTypes.func, + saveQuizContent: PropTypes.func, + saveDB: PropTypes.func, + load: PropTypes.func, + saveMedia: PropTypes.func, + removeMedia: PropTypes.func, + saveThumb: PropTypes.func +} diff --git a/javascript/Edit/Quiz/AnswerBlock.jsx b/javascript/Edit/Quiz/AnswerBlock.jsx new file mode 100644 index 0000000..6aade8b --- /dev/null +++ b/javascript/Edit/Quiz/AnswerBlock.jsx @@ -0,0 +1,94 @@ +'use strict' +import React, { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import { + Form +} from 'react-bootstrap' +import Tippy from '@tippyjs/react'; +import './quiz.css' + +const {Row, Group, Check , Control} = Form + +// prop.types may equal 'choice' or 'select' +export default function AnswerBlock(props) { + + const [fb, setFb] = useState('') + + useEffect(() => { + const feed = [...props.feedback] // This will represent ['local' | 'global, globalField1, globalField2, ...arrayOfLocalFeedback] + setFb(feed[props.id+3]) + }, []) + + useEffect(() => { + let feed = [...props.feedback] + feed[props.id + 3] = fb + if (props.custom) { + feed[0] = 'local' + } + props.setFeedback(feed) + }, [fb]) + + function _delete() { + props.remove(props.id) + } + + const customAnswerRow = ( + + + setFb(e.target.value)} + /> + + + Your custom message will be shown when user selects this answer} arrow={true}> + + + + + ) + + return ( + + + + + + + + + + Remove Answer} arrow={true}> + + + + + + + {(props.custom) ? customAnswerRow : null} + + ) +} + +AnswerBlock.propTypes = { + id: PropTypes.number, + onChange: PropTypes.func, + placeholder: PropTypes.string, + checked: PropTypes.bool, + type: PropTypes.string +} diff --git a/javascript/Edit/Quiz/AnswerSettings.jsx b/javascript/Edit/Quiz/AnswerSettings.jsx new file mode 100644 index 0000000..cc6bb9e --- /dev/null +++ b/javascript/Edit/Quiz/AnswerSettings.jsx @@ -0,0 +1,100 @@ +import React, { useState, useEffect } from 'react' +import { Modal } from 'react-bootstrap' +import Tippy from '@tippyjs/react' + +const {Header, Body, Footer} = Modal + +export default function AnswerSettings(props) { + + const {feedback, setFeedback} = props + + const [localEnable, setLocalEnable] = useState(false) + + useEffect(() => { + let disable = false + if (feedback[0] === 'local') { + disable = true + } + setLocalEnable(disable) + }, [feedback]) + + async function save() { + // I have this a seperate function for the case we add more settings here + // that we may want to save seperatly. + await saveFeedback() + // Other settings to be saved... + props.onHide() + } + + async function saveFeedback() { + // TODO: We should probs do an ajax, but I was thinking we could just call the parent saveDb function + // But for right now, it will just be saved to the state and when editQuiz is cloesed the feedback will be saved. + console.log("Saving settings") + } + + function handleChange(e) { + // I'm not sure the iterator spread is necessary but from my pointer ptsd imma do it anyway lmao + let f = [...feedback] + f[0] = 'global' + if (e.currentTarget.id === 'onCorrect') { + f[1] = e.currentTarget.value + } + else if (e.currentTarget.id === 'onIncorrect') { + f[2] = e.currentTarget.value + } + setFeedback(f) + } + + const handleGlobal = function(e) { + console.log("test") + props.toggleGlobal() + setLocalEnable(!localEnable) + } + + const answerFeedbackHelp = ( +
    + Allows for custom feedback to be displayed to the user when submititing an answer. +

    + If they choose the correct answer the on-correct message will appear and vice-versa +

    + These will not appear if the local custom answer responses are enabled and filled out +
    + ) + + return ( + +
    +

    Answer Settings

    +
    + +
    +
    Generic Answer Responses
    +
    + + +
    +
    +
    +
    + On Correct
    } arrow={true}> + + +
    + +
    +
    +
    + On Incorrect
    } arrow={true}> + + +
    + + + + +
    + +
    +
    + ) +} \ No newline at end of file diff --git a/javascript/Edit/Quiz/AnswerTypeCards.jsx b/javascript/Edit/Quiz/AnswerTypeCards.jsx new file mode 100644 index 0000000..6e45724 --- /dev/null +++ b/javascript/Edit/Quiz/AnswerTypeCards.jsx @@ -0,0 +1,54 @@ +'use strict' +import React, { Component } from 'react' +import PropTypes from 'prop-types' + + +export default class AnswerTypeCards extends Component { + constructor() { + super() + } + + render() { + return ( +
    +
    +
    +
    Multiple Choice
    +
    +

    Multiple choice options with one possible correct answer

    + +
    +
    +
    + {/* Open Answer +
    +
    +
    Open Answer
    +
    +

    Open answer field for user's open response

    +

    + +
    +
    +
    + */} +
    +
    +
    Multiple Select
    +
    +

    Multiple choice options with one or more possible correct answer(s)

    + +
    +
    +
    +
    + + ) + } +} + +AnswerTypeCards.propTypes = { + openAnswer: PropTypes.func, + multipleChoice: PropTypes.func, + multipleSelect: PropTypes.func +} diff --git a/javascript/Edit/Quiz/OpenAnswerBlock.jsx b/javascript/Edit/Quiz/OpenAnswerBlock.jsx new file mode 100644 index 0000000..41123d9 --- /dev/null +++ b/javascript/Edit/Quiz/OpenAnswerBlock.jsx @@ -0,0 +1,79 @@ +'use strict' +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { + Form, + Button, + ButtonToolbar, + ButtonGroup, + Row, + Col, + Card, + InputGroup, + FormControl +} from 'react-bootstrap' + +import Tippy from '@tippyjs/react' + +export default class OpenAnswerBlock extends Component { + constructor(props) { + super(props) + this.state = { + placeholder: null, + id: props.id, + value: props.value + } + this.onChangeValue = this.onChangeValue.bind(this) + } + + componentDidMount() { + let place = this.props.placeholder + if (place == null) { + place = "Students will put their answer here. If their answer should be graded on correctness, then select the checkbox on the right." + } + this.setState({ + placeholder: place + }) + + } + + onChangeValue(event) { + this.setState({ + value: event.target.value + }) + this.props.onChange(event) + } + + render() { + return ( + + + + Options + + + + + + + + + + + ) + } +} + +OpenAnswerBlock.propType = { + //key: PropTypes.number, + onChange: PropTypes.func, + placeholder: PropTypes.value, + id: PropTypes.number +} diff --git a/javascript/Edit/Quiz/QuestionTitleBlock.jsx b/javascript/Edit/Quiz/QuestionTitleBlock.jsx new file mode 100644 index 0000000..1126c2d --- /dev/null +++ b/javascript/Edit/Quiz/QuestionTitleBlock.jsx @@ -0,0 +1,53 @@ +'use strict' +import React, { Component } from 'react' + +export default class QuestionTitleBlock extends Component { + constructor(props) { + super(props) + + this.state = { + title: '' + } + this.onChange = this.onChange.bind(this) + } + + componentDidMount() { + this.setState({ title: this.props.value }) + } + + componentDidUpdate(prevProps) { + if (prevProps.value != this.props.value && this.props.value != undefined) { + this.setState({ title: this.props.value }) + } + } + + onChange(event) { + this.setState({ + title: event.target.value + }) + this.props.onChange(event) + } + + render() { + return ( +
    +

    Question

    + +
    + ) + } +} + + +const formControlStyle = { + textAlign: 'center', + marginLeft: 'auto', + marginRight: 'auto', + width: '60%' +} diff --git a/javascript/Edit/Quiz/QuizEdit.jsx b/javascript/Edit/Quiz/QuizEdit.jsx new file mode 100644 index 0000000..a0c57f7 --- /dev/null +++ b/javascript/Edit/Quiz/QuizEdit.jsx @@ -0,0 +1,365 @@ +import React, { useEffect, useState } from 'react' + +import AnswerTypeCards from './AnswerTypeCards.jsx' +import QuestionTitle from './QuestionTitleBlock.jsx' +import AnswerBlock from './AnswerBlock.jsx' +import SettingsModal from './AnswerSettings' + +import './quiz.css' +import { saveQuiz } from '../../api/quiz.js' + +import 'animate.css' +import Tippy from '@tippyjs/react' +import { Form, Modal } from 'react-bootstrap' +import Dropzone from 'react-dropzone-uploader' +import { propTypes } from 'react-bootstrap/esm/Image' +import PropTypes from "prop-types" +const { Row, Group, Check } = Form + +export default function QuizEdit(props) { + + const [question, setQuestion] = useState('') + const [answers, setAnswers] = useState(['', '']) // : string[] + const [correct, setCorrect] = useState([]) // : number[] + const [type, setType] = useState('showTypes') + const [feedback, setFeedback] = useState(['global', 'Correct!', 'Please try again']) // : string[] + const [id, setId] = useState(-1) + const [imageUrl, setImageUrl] = useState('') + + const [showModal, setShowModal] = useState(false) + const [showCustom, setShowCustom] = useState(false) + const [feedCheck, setFeedCheck] = useState(false) + + const [animationType, setAnimation] = useState('animated slideInRight faster') + + + useEffect(() => { + let initQuestion = '' + let initAnswers = ['', ''] + let initCorrect = [] + let initType = 'showTypes' + let initFeedback = ['global', 'Correct!', 'Please try again'] + let initId = window.sessionStorage.getItem('quizId') + + let initFeedCheck = false + // This will only be true if the slide is empty + if (props.quizContent != null && props.quizContent.correct != null) { + // Hooks are not allowed to be called in conditonals which is why there is this horrible code structure here + initQuestion = props.quizContent.question + initAnswers = props.quizContent.answers + initCorrect = props.quizContent.correct + initType = props.quizContent.type + initFeedback = props.quizContent.feedback + initId = props.quizContent.quizId + initFeedCheck = (initFeedback[0] === 'local') + } + setQuestion(initQuestion) + setAnswers(initAnswers) + setCorrect(initCorrect) + setType(initType) + setFeedback(initFeedback) + setId(initId) + setFeedCheck(initFeedCheck) + setShowCustom(initFeedCheck) + }, []) + + + async function save() { + if (correct.length === 0) { + alert("Please select a correct answer.") + return + } else if (type === 'choice' && feedCheck) { + // Checks to make sure all of feeback is filled out + for (let i = 2; i < feedback.length; i++) { + if (feedback[i] == undefined || feedback[i] == "") { + alert("Please ensure that all custom answers are filled out.") + return + } + } + } + + let quizContent = { + 'quizId': id, + 'question': question, + 'answers': answers, + 'correct': correct, + 'type': type, + 'feedback': feedback, + 'imageUrl': imageUrl + } + const saved = await saveQuiz(id, quizContent) + + if (saved) { + props.saveQuizContent(quizContent) + await props.load() + } else { + alert("an error has occurred when saving") + } + + props.toggle() + } + + function handleAnswerChange(e) { + const ids = e.target.id.split('-') + const type = ids[0] + const i = Number(ids[1]) + let a = [...answers] + let c = [...correct] + if (type == 'text') { + a[ids[1]] = e.target.value + } + else if (type === 'choice') { + // This is multiple choice and there is only one correct answer + c[0] = i + } + else if (type === 'select') { + // handle change + // if the is is in the array we need to remove it, if the id is not we add it + console.log(ids) + if (c.includes(i) || c.includes(i.toString())) { + // remove current choice from correct + let item = c.findIndex(index => { return index == i }) + c.splice(item, 1) + } + else { + // Add new choice to correct + c.push(i) + } + } + + setAnswers(a) + setCorrect(c) + } + + function switchView(e) { + setType(e.target.id) + } + + function addAnswer() { + let a = [...answers] + a.push('') + setAnswers(a) + } + + function removeAnswer(id) { + + let a = [...answers] + let c = [...correct] + let f = [...feedback] + + if (c.includes(id.toString())) { + const index = c.indexOf(id.toString()) + c.splice(index, 1) + } + a.splice(id, 1) + f.splice(id + 3, 1) + setAnswers(a) + setCorrect(c) + setFeedback(f) + } + + function toggleFeedCheck() { + let f = [...feedback] + let fc = (f[0] === 'local') + if (type === 'select') { + // This shouldn't get triggered since I remove the ui option but I will leave it here in case + alert("Custom Local Feedback is not supported for multiple select") + fc = true + } + f[0] = (fc) ? 'global' : 'local' + setFeedback(f) + setFeedCheck(!fc) + setShowCustom(!fc) + } + + function changeBackground(fileWithMeta) { + let formData = new FormData() + const showId = Number(window.sessionStorage.getItem('id')) + const slideId = Number(window.sessionStorage.getItem('slideId')) + let fMeta = fileWithMeta[0] + formData.append('backgroundMedia', fMeta.file) + formData.append('slideId', slideId) + formData.append('id', showId) + + $.ajax({ + url: './slideshow/Slide/background/' + slideId, + type: 'post', + data: formData, + processData: false, + contentType: false, + success: (imageUrl) => { + props.changeBackground(imageUrl) + setImageUrl(imageUrl) + alert('Background Image has been Inserted!') + }, + error: (req, res) => { + console.error(res) + alert( + 'An error has occured with this image. Please try a different image.' + ) + }, + }) + } + + function buildAnswerBlock(type) { + if (type !== 'choice' && type != 'select') return undefined + let i = -1 + let choices = answers.map((choice) => { + i++ + let checked = correct.includes(i.toString()) || correct.includes(i) + return setFeedback(f)} + feedback={feedback} + /> + }) + + let bottomBlock = ( + + + + + + Answer Settings} arrow={true}> + setShowModal(true)}> + + + {type === 'choice' ? + + {!showCustom ? 'Show ' : 'Hide '} Custom Answer responses} arrow={true}> +
    setShowCustom(!showCustom)}> + +
    +
    +
    : null} + {type === 'choice' ? // Only support custom feedback on multipleChoice for now + + + {/**/} + + : null} +
    + ) + choices.push(bottomBlock) + return choices + } + + let imgModal = ( + + +
    Insert Image
    +
    + +
    +
    Upload
    + +
    +
    +
    + ) + + let imageBlock = (type === 'showTypes') ? + undefined : + ( + + + +
    +
    Insert background Image
    + +
    +
    +
    + ) + + let answerTypeBlock = type === 'showTypes' ? ( + + ) : null + + let showAddElement = (type === 'showTypes') ? + undefined : + ( + + + ) + + + const quizBuild = buildAnswerBlock(type) + + return ( +
    +

    Edit Quiz

    +
    + setQuestion(e.currentTarget.value)} id={0} /> +
    + {quizBuild} + {imgModal} + {imageBlock} + {answerTypeBlock} + {showAddElement} +
    + setShowModal(false)} + setFeedback={(f) => setFeedback(f)} + feedback={feedback} + toggleGlobal={toggleFeedCheck} + /> +
    + ) +} + +const containerStyle = { + padding: '20px', + backgroundColor: '#E5E7E9', + borderRadius: 3 +} + +QuizEdit.propTypes = { + validate: PropTypes.func, + insertMedia: PropTypes.func, + mediaView: PropTypes.bool, + mediaCancel: PropTypes.func, + mediaOpen: PropTypes.func +} \ No newline at end of file diff --git a/javascript/Edit/Quiz/QuizView.jsx b/javascript/Edit/Quiz/QuizView.jsx new file mode 100644 index 0000000..ff1dfd2 --- /dev/null +++ b/javascript/Edit/Quiz/QuizView.jsx @@ -0,0 +1,68 @@ +'use strict' +import React, {Component} from 'react' +import PropTypes from 'prop-types' + +export default class QuizView extends Component { + constructor(props) { + super(props) + } + + render() { + let answers = undefined + if (this.props.quizContent.answers != null) { + //console.log(this.props.quizContent) + // TODO: fix bug with icon not changing when correct does. We need to force a rerender + let type = this.props.quizContent.type === 'select' ? 'square' : 'circle' + answers = this.props.quizContent.answers.map( + function (question, i) { + // random key to ensure that the icons get rerendered each time + const k = + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15) + let check = ( + + ) + if (this.props.quizContent.correct != undefined) { + if (this.props.quizContent.correct.includes(i.toString())) { + check = ( + + ) + } + } + return ( +
    +

    + {check} {question} +

    +
    + ) + }.bind(this) + ) + } else { + answers = ( +
    +

    This slide may have been corrupted. We apolgize for the error.

    +
    + ) + } + // TODO: rework this to be more pretty + // Also need to handle open answers + let title = + !this.props.quizContent.question.length > 0 + ? 'No data loaded' + : this.props.quizContent.question + return ( +
    +

    {title}

    + {answers} +
    + ) + } +} + +QuizView.propTypes = { + quizContent: PropTypes.object, + toggle: PropTypes.func, +} diff --git a/javascript/Edit/Quiz/quiz.css b/javascript/Edit/Quiz/quiz.css new file mode 100644 index 0000000..1263c49 --- /dev/null +++ b/javascript/Edit/Quiz/quiz.css @@ -0,0 +1,41 @@ +.custRow15 { + margin-left: 15%; +} + +.custRow10 { + margin-left: 10%; +} + +.flexbox { + /*border: 1px solid;*/ + display: flex; + flex: 1; + justify-content: flex-start; + align-items: end; +} + +.flexbox-mid { + display: flex; + flex: 1; + justify-content: flex-end; + align-items: center; +} + +.custResponse { + width: 80%; + margin-right: 1rem; +} + +.ans { + width: 60%; + margin-right: 1rem; +} + +.ss-danger { + color: #d9534f; +} + +.dg-small { + color: dimgray; + font-size: 18px; +} \ No newline at end of file diff --git a/javascript/Edit/Settings.jsx b/javascript/Edit/Settings.jsx new file mode 100644 index 0000000..149c42f --- /dev/null +++ b/javascript/Edit/Settings.jsx @@ -0,0 +1,228 @@ +'use strict' +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import { + Button, + ButtonGroup, + Modal, + Form, + Row, + Col, + OverlayTrigger, + Popover, +} from 'react-bootstrap' +import {CirclePicker, SketchPicker} from 'react-color' +import AnimateDropdown from './AddOn/AnimateDropdown' +import './custom.css' +/* global $ */ + +export default class Settings extends Component { + constructor(props) { + super(props) + this.state = { + settings: false, + slideTimer: '2s', + displaySketchPicker: false, + } + + this.toggleSettings = this.toggleSettings.bind(this) + this.handleColorChange = this.handleColorChange.bind(this) + this.changeTime = this.changeTime.bind(this) + this.handleSketchPicker = this.handleSketchPicker.bind(this) + } + + componentDidUpdate(prevProps) { + if (prevProps.slideTimer != this.props.slideTimer) { + this.setState({ + slideTimer: String(this.props.slideTimer) + 's', + }) + } + } + + toggleSettings() { + this.setState({settings: !this.state.settings}) + } + + handleColorChange(color) { + this.props.saveBackground(color.hex) + } + + changeTime(event) { + // converts to number + this.setState({slideTimer: event.target.value}) + const time = parseInt(event.target.value, 10) + $.ajax({ + url: './slideshow/Show/' + this.props.id, + data: {slideTimer: time}, + type: 'put', + error: (req, res) => { + console.error(req, res.toString()) + }, + }) + } + + handleSketchPicker() { + this.setState({displaySketchPicker: !this.state.displaySketchPicker}) + } + + render() { + let colorPick + if (this.state.displaySketchPicker == true) { + colorPick = ( +
    + +
    + ) + } + let colorPickStyle = this.state.hover + ? {borderColor: this.props.currentColor} + : {color: this.props.currentColor} + return ( + + + +
    + + Settings + + + + + + Slide Timer + + + Select required wait time for each slide before + continuing to the next slide. (Does not apply to + quizzes) + + }> + + + + + + + + + + + + + + + + +

    + + + Background Color + + + + + + this.setState({hover: true})} + onMouseLeave={() => this.setState({hover: false})}> + + Click to enter custom color + + }> + + + + {colorPick} + + +

    + + + Slide Animation + + + + + + + Preview + + + +
    +
    +
    +
    + ) + } +} + +Settings.propTypes = { + slideTimer: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + saveBackground: PropTypes.func, + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + currentColor: PropTypes.string, + animation: PropTypes.string, + setAnimation: PropTypes.func, +} diff --git a/javascript/Edit/SlidesView.jsx b/javascript/Edit/SlidesView.jsx new file mode 100644 index 0000000..cdc1b4f --- /dev/null +++ b/javascript/Edit/SlidesView.jsx @@ -0,0 +1,53 @@ +'use strict' +import React, { Component } from 'react' +import ButtonGroup from 'canopy-react-buttongroup' +import PropTypes from 'prop-types' + +export default class SlidesView extends Component { + constructor(props) { + super(props) + this.state = { + currentSlide: this.props.currentSlide + } + this.handleSlide = this.handleSlide.bind(this) + this.handleNewSlide = this.handleNewSlide.bind(this) + } + + handleSlide(event) { + this.props.setCurrentSlide(event.target.value - 1) + } + + handleNewSlide() { + this.props.addNewSlide() + this.props.setCurrentSlide(this.props.currentSlide + 1) + } + + render() { + let slideCount = 0 + let data = this.props.slides.map(function(slide) { + slideCount += 1 + let bClass = (this.props.currentSlide + 1 == slideCount) ? "btn btn-outline-secondary active" : "btn btn-outline-secondary" + return( + + ) + }.bind(this)); + + return ( +
    +

    +
    + {data} + +
    +
    + ) + } + +} + +SlidesView.propTypes = { + slides: PropTypes.array, + currentSlide: PropTypes.number, + setCurrentSlide: PropTypes.func, + addNewSlide: PropTypes.func, +} diff --git a/javascript/Edit/Toolbar/Background.jsx b/javascript/Edit/Toolbar/Background.jsx new file mode 100644 index 0000000..ef19e28 --- /dev/null +++ b/javascript/Edit/Toolbar/Background.jsx @@ -0,0 +1,96 @@ +import React, {useState} from 'react' +import Tippy from '@tippyjs/react' +import 'tippy.js/themes/light-border.css' +import {Modal, Row, Col} from 'react-bootstrap' +//import {CirclePicker} from 'react-color' +import ColorSelect from '../AddOn/ColorSelect' +import Dropzone from 'react-dropzone-uploader' +import PropTypes from 'prop-types' + +/* global $ */ + +const {Header, Body} = Modal + +export default function Background(props) { + const [modalView, setModal] = useState(false) + + function insertMedia(fileWithMeta) { + let formData = new FormData() + const showId = Number(window.sessionStorage.getItem('id')) + const slideId = Number(window.sessionStorage.getItem('slideId')) + let fMeta = fileWithMeta[0] + formData.append('backgroundMedia', fMeta.file) + formData.append('slideId', slideId) + formData.append('id', showId) + + $.ajax({ + url: './slideshow/Slide/background/' + slideId, + type: 'post', + data: formData, + processData: false, + contentType: false, + success: (imageUrl) => { + props.changeBackground(imageUrl) + }, + error: (req, res) => { + console.error(res) + alert( + 'An error has occured with this image. Please try a different image.' + ) + }, + }) + } + + function validate({meta}) { + if (meta.status === 'rejected_file_type') { + alert('Sorry, this file type is not supported') + } + } + + const modalRender = ( + setModal(false)} size="lg"> +
    +
    Change Background
    +
    + + + + +
    Image
    + +
    +
    +
    Upload
    + +
    + +
    + ) + + return ( + + {modalRender} + + + ) +} + +Background.propTypes = { + changeBackground: PropTypes.func, +} diff --git a/javascript/Edit/Toolbar/Link.jsx b/javascript/Edit/Toolbar/Link.jsx new file mode 100644 index 0000000..decbde9 --- /dev/null +++ b/javascript/Edit/Toolbar/Link.jsx @@ -0,0 +1,108 @@ +'use strict' +import React, { Component } from 'react' +import './toolbar.css' + +import { EditorState, RichUtils} from 'draft-js' +import decorator from '../../Resources/Draft/LinkDecorator' + +import Tippy from '@tippyjs/react' +import 'tippy.js/themes/light-border.css' + +export default class Link extends Component { + constructor(props) { + super(props) + this.state = { + link: '', + selected: 'Select some text' + } + + this.updateLink = (event) => {this.setState({link: event.target.value})} + + this.insertLink = this.insertLink.bind(this) + this.updateSelected = this.updateSelected.bind(this) + } + + insertLink() { + const editorState = this.props.getEditorState() + const contentState = editorState.getCurrentContent() + + const contentWithEntity = contentState.createEntity( + 'LINK', + 'IMMUTABLE', // This causes the entire link text to delete + {url: this.state.link} + ) + const entityKey = contentWithEntity.getLastCreatedEntityKey() + + let newEditor = EditorState.set(editorState, {currentContent: contentWithEntity, decorator: decorator}) + + newEditor = RichUtils.toggleLink(newEditor, newEditor.getSelection(), entityKey) + //newEditor = RichUtils.toggleInlineStyle(newEditor, 'underline')*/ + this.props.setEditorState(newEditor) + } + + updateSelected() { + const editorState = this.props.getEditorState() + const contentState = editorState.getCurrentContent() + const selectionState = editorState.getSelection() + + let select = "Please select some text" + if (!selectionState.isCollapsed()) { + // Text is selected + const key = selectionState.getAnchorKey() + select = contentState.getBlockForKey(key).getText() + const start = selectionState.getStartOffset() + const end = selectionState.getEndOffset() + select = select.slice(start, end) + } + + this.setState({ + selected: select + }) + } + + render() { + let linkPopover = ( +
    + +
    Insert a url
    + Selected Text +
    + +
    +
    + +
    + + +
    + ) + + return ( + + + + + + ) + } +} + +const inputStyle = { + //borderRadius: 10, + textAlign: 'center', + color: 'royalblue' +} + +const selectedText = { + textAlign: 'center', + color: 'darkslategrey', + borderRadius: 10 +} \ No newline at end of file diff --git a/javascript/Edit/Toolbar/Media.jsx b/javascript/Edit/Toolbar/Media.jsx new file mode 100644 index 0000000..b97db76 --- /dev/null +++ b/javascript/Edit/Toolbar/Media.jsx @@ -0,0 +1,78 @@ +'use strict' +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import {Modal} from 'react-bootstrap' +import Tippy from '@tippyjs/react' +import Dropzone from 'react-dropzone-uploader' +import './toolbar.css' +import 'react-dropzone-uploader/dist/styles.css' +import 'tippy.js/themes/light-border.css' + +/* global $ */ + +export default class Media extends Component { + constructor(props) { + super(props) + + this.state = { + mediaView: false, + imageUrl: '', + } + + this.mediaModal = this.mediaModal.bind(this) + this.mediaCancel = this.mediaCancel.bind(this) + } + + + mediaModal() { + this.setState({mediaView: true}) + } + + mediaCancel() { + this.setState({mediaView: false}) + } + + render() { + let mediaModal = ( + + +
    Insert Image
    +
    + +
    +
    Upload
    + +
    +
    +
    + ) + + return ( + + {mediaModal} + + + ) + } +} + +Media.propTypes = { + validate: PropTypes.func, + insertMedia: PropTypes.func +} diff --git a/javascript/Edit/Toolbar/QuizToolbar.jsx b/javascript/Edit/Toolbar/QuizToolbar.jsx new file mode 100644 index 0000000..4c1b657 --- /dev/null +++ b/javascript/Edit/Toolbar/QuizToolbar.jsx @@ -0,0 +1,17 @@ +'use strict' +import React from 'react' + +export default function QuizToolbar(props) { + + const edit = () + + const cancel = () + + return ( +
    + {props.view ? cancel : edit} +
    + ) +} \ No newline at end of file diff --git a/javascript/Edit/Toolbar/TextColor.jsx b/javascript/Edit/Toolbar/TextColor.jsx new file mode 100644 index 0000000..56d2137 --- /dev/null +++ b/javascript/Edit/Toolbar/TextColor.jsx @@ -0,0 +1,150 @@ +'use strict' +import React, {Component} from 'react' +import Tippy from '@tippyjs/react' +import TextColorMap from '../../Resources/Draft/TextColorMap' +import PropTypes from 'prop-types' +import {EditorState, RichUtils, Modifier, SelectionState} from 'draft-js' +import {CirclePicker, ChromePicker} from 'react-color' + +import './toolbar.css' +import 'tippy.js/themes/light-border.css' + +export default class TextColor extends Component { + constructor(props) { + super(props) + + this.state = { + color: 'black', + selection: SelectionState.createEmpty(), + anchorKey: undefined, + showAdvanced: false, + } + + this.changeColor = this.changeColor.bind(this) + this.toggleColor = this._toggleColor.bind(this) + } + + changeColor(color) { + this.setState({color: color.hex}) + this.toggleColor(color.hex) + } + + _toggleColor(toggledColor) { + const editorState = this.props.getEditorState() + let selection = editorState.getSelection() + let anchorKey = selection.getAnchorKey() + + if ( + this.state.anchorKey != undefined && + anchorKey != this.state.anchorKey + ) { + // dump cache bc slides/or content block was changed + anchorKey = this.state.anchorKey + } + + if (selection.isCollapsed()) { + // Nothing is selected so we use the previous selection + selection = this.state.selection + if (selection.isCollapsed()) { + alert('Please select some text to apply this color') + } + } + // Cache Selection and its Key + this.setState({selection: selection, anchorKey: anchorKey}) + + // Advanced Color + const color = editorState.getCurrentInlineStyle().keys().next().value + const styleAddon = '{"' + color + '":{"color":"' + color + '"}}' + const styles = Object.assign(JSON.parse(styleAddon), TextColorMap) + + // remove other colors first + let mapping = Object.keys(styles).map((style) => { + if (style.startsWith('#')) return style + }) + let newContentState = mapping.reduce((contentState, color) => { + return Modifier.removeInlineStyle(contentState, selection, color) + }, editorState.getCurrentContent()) + + let newEditorState = EditorState.push( + editorState, + newContentState, + 'change-inline-style' + ) + + const currStyle = editorState.getCurrentInlineStyle() + + // If nothing is currently selected + + if (!currStyle.has(toggledColor)) { + newEditorState = RichUtils.toggleInlineStyle(newEditorState, toggledColor) + } + + this.props.setEditorState(EditorState.moveFocusToEnd(newEditorState)) + } + + render() { + let advancedColor = this.state.showAdvanced ? ( +
    + +
    + ) : undefined + + let colorPopover = ( +
    +
    Adjust Text Color
    + +

    +
    + + {advancedColor} +
    +
    + ) + + return ( + + + + + + ) + } +} + +TextColor.propTypes = { + getEditorState: PropTypes.func, + setEditorState: PropTypes.func, +} + +const popoverStyle = { + padding: 10, + marginLeft: 'auto', + marginRight: 'auto', +} + +const advancedColorStyle = { + paddingTop: 10, + marginLeft: 10, +} diff --git a/javascript/Edit/Toolbar/Toolbar.jsx b/javascript/Edit/Toolbar/Toolbar.jsx new file mode 100644 index 0000000..15f0664 --- /dev/null +++ b/javascript/Edit/Toolbar/Toolbar.jsx @@ -0,0 +1,214 @@ +'use strict' +import React from 'react' +import TextColor from './TextColor' +import Media from './Media' +import Link from './Link' +import {EditorState, RichUtils, Modifier} from 'draft-js' +import Tippy from '@tippyjs/react' +import Background from './Background' +import PropTypes from 'prop-types' +import './toolbar.css' +import 'tippy.js/themes/light-border.css' + +/** In this file, I have most of the buttons as function components. At the bottom, I have a final composite Toolbar component. */ + +/** Undo and Redo */ +function UndoRedo(props) { + function _undo() { + props.setEditorState(EditorState.undo(props.getEditorState())) + } + + function _redo() { + props.setEditorState(EditorState.redo(props.getEditorState())) + } + + return ( + + + + + + ) +} + +UndoRedo.propTypes = { + getEditorState: PropTypes.func, + setEditorState: PropTypes.func, +} + +/** Bold and Italic */ +function BoldItalic(props) { + function _toggleInlineStyle(event) { + const eState = props.getEditorState() + props.setEditorState( + RichUtils.toggleInlineStyle(eState, event.currentTarget.id) + ) + } + + return ( + + + + + ) +} +BoldItalic.propTypes = { + getEditorState: PropTypes.func, + setEditorState: PropTypes.func, +} + +/** Blocks */ +function Blocks(props) { + function _toggleBlockType(event) { + const eState = RichUtils.toggleBlockType( + props.getEditorState(), + event.currentTarget.id + ) + props.setEditorState(EditorState.moveFocusToEnd(eState)) + } + + return ( + + + + + + + + ) +} +Blocks.propTypes = { + getEditorState: PropTypes.func, + setEditorState: PropTypes.func, +} + +/** Alignment */ +function Alignment(props) { + function _alignBlock(event) { + const toggledAlignment = 'align-' + event.currentTarget.id + const editorState = props.getEditorState() + const contentState = editorState.getCurrentContent() + const selection = editorState.getSelection() + let currBlock = contentState.getBlockForKey(selection.getFocusKey()) + const bData = currBlock.getData().set('align', toggledAlignment) + const newContentState = Modifier.setBlockData( + contentState, + selection, + bData + ) + const newEditorState = EditorState.push( + editorState, + newContentState, + 'change-block-data' + ) + props.setEditorState(newEditorState) + } + + return ( + + + + + + ) +} + +Alignment.propTypes = { + getEditorState: PropTypes.func, + setEditorState: PropTypes.func, +} + +export default function Toolbar(props) { + return ( +
    + + + + + + + + + + + + +
    + ) +} + +Toolbar.propTypes = { + getEditorState: PropTypes.func, + setEditorState: PropTypes.func, + saveBackground: PropTypes.func, + saveMedia: PropTypes.func, + insertMedia: PropTypes.func, + validate: PropTypes.func, + mediaView: PropTypes.bool, + mediaCancel: PropTypes.func, + mediaOpen: PropTypes.func +} diff --git a/javascript/Edit/Toolbar/toolbar.css b/javascript/Edit/Toolbar/toolbar.css new file mode 100644 index 0000000..e47a0ac --- /dev/null +++ b/javascript/Edit/Toolbar/toolbar.css @@ -0,0 +1,50 @@ +.tool { + width: 100%; + border: 1px solid #ddd; + background: #fff; + border-radius: 2px; + box-shadow: 0px 1px 3px 0px rgba(220,220,220,1); + z-index: 2; + box-sizing: border-box; +} + +.separator { + border-left: 1px solid #ddd; + border-right: 1px solid #ddd; + height: 35px; + position: absolute; + /* + If we want the separator to float center + top: 19px; + height: 30px;*/ +} + +/* Button Styles */ +button.toolbar { + background: #fbfbfb; + color: #888; + font-size: 18px; + border: 0; + padding-top: 0px; + vertical-align: bottom; + height: 34px; + width: 36px; +} + +button.toolbar:hover, button.toolbar:focus { + background: #f3f3f3; + outline: 0; /* reset for :focus */ +} + +i { + fill: #888; +} + +.drop +{ + width: 90%; + margin-left:auto; + margin-right:auto; +} + + diff --git a/javascript/Edit/custom.css b/javascript/Edit/custom.css new file mode 100644 index 0000000..c337e27 --- /dev/null +++ b/javascript/Edit/custom.css @@ -0,0 +1,45 @@ +.cust-col-11 { + flex: 0 0 96.66667%; + max-width: 96.66667%; +} + +.cust-col-1 { + flex: 0 0 3.33333%; + max-width: 3.33333%; +} + +.settings-options { + font-size: 20px; +} + +.linkDecorator { + text-decoration-line: underline !important; + text-decoration-color: royalblue; +} + +.ColorPicker { + margin-top: 35px; + margin-left: 23px; + border-radius: 20px; + height: 40px; + width: 40px; + background-color: transparent; + border: 1px solid transparent; +} + +.ColorPicker:hover { + border: 5px solid; +} + +.SketchPicker { + padding: 0rem; +} + +.align-right { + float: none; + text-align: right; +} +.align-center { + float: none; + text-align: center; +} diff --git a/javascript/ShowForm/index.jsx b/javascript/Edit/index.jsx similarity index 52% rename from javascript/ShowForm/index.jsx rename to javascript/Edit/index.jsx index 1d53d7c..4fc4929 100644 --- a/javascript/ShowForm/index.jsx +++ b/javascript/Edit/index.jsx @@ -1,7 +1,7 @@ 'use strict' import React from 'react' import ReactDOM from 'react-dom' -import Show from './Show.jsx' +import Edit from './Edit.jsx' ReactDOM.render( - , document.getElementById('showform')) + , document.getElementById('edit')) diff --git a/javascript/Present/Navbar.jsx b/javascript/Present/Navbar.jsx new file mode 100644 index 0000000..a3e6977 --- /dev/null +++ b/javascript/Present/Navbar.jsx @@ -0,0 +1,164 @@ +import React, {useState, useEffect} from 'react' + +import Tippy from '@tippyjs/react' +import 'tippy.js/themes/light-border.css' + +export const Navigation = (props) => { + return ( +
    +
    + + +
    +
    + ) +} + +export const NavButton = (props) => { + let type = 'secondary' + if (props.type != undefined) type = props.type + return ( +
    + +
    + ) +} + +export const Finish = (props) => { + let visible = true + if (props.visible != undefined) visible = props.visible + if (!visible) return null + return ( + (window.location.href = './slideshow/Show')} + /> + ) +} + +export const SlidesNav = (props) => { + const [popover, setPopover] = useState(false) + + //useEffect(() => console.log(popover), [popover]) + + function changeSlide(event) { + const id = event.currentTarget.id + if (id == 'high') { + props.changeSlide(props.high) + } else if (id === 'first') { + props.changeSlide(0) + } else { + props.changeSlide(id - 1) + } + setPopover(false) + } + + let closeSlides = [] + for (let i = props.currentSlide - 2; i < props.currentSlide + 3; i++) { + if (i < 0 || i > props.high || i > props.max) continue + closeSlides.push(i + 1) + } + const closeButtonG = closeSlides.map((i) => { + let type = i - 1 === props.currentSlide ? 'primary' : 'secondary' + return ( + + ) + }) + const slidesCon = ( +
    +

    Change Slide

    +
    {closeButtonG}
    +
    +

    Return to

    +
    +
    + +
    +
    + +
    +
    +
    + +
    + ) + // One Tippy is onHover, the other is onClick + return ( + + + setPopover(true)} + onBlur={() => setPopover(false)} + /> + + + ) +} + +export const Progress = (props) => { + return ( +
    +
    + {props.value} +
    +
    + ) +} + +const buttonGroup = { + justifyContent: 'center', + display: 'flex', + marginBottom: '1rem', +} diff --git a/javascript/Present/Present.jsx b/javascript/Present/Present.jsx new file mode 100644 index 0000000..0ded463 --- /dev/null +++ b/javascript/Present/Present.jsx @@ -0,0 +1,203 @@ +import React, {useState, useEffect} from 'react' + +// Resources for loading from db +import { + fetchShow, + fetchSlides, + fetchSession, + updateSession, + slidesResource, +} from '../api/present' + +import {getPageId} from '../api/getPageId' + +import PresentView from './PresentView' + +import {Progress, Navigation, Finish, SlidesNav} from './Navbar' +import Skeleton from '../Resources/Components/Skeleton' +import PropTypes from 'prop-types' + +import 'animate.css' + +export default function Present({isAdmin}) { + const [showTitle, setShowTitle] = useState('Present: ') + const [showTimer, setShowTimer] = useState(0) + const [showAnimation, setShowAnimation] = useState('None') + const [noShow, setNoShow] = useState(true) + + const [content, setContent] = useState(slidesResource.content) + + const [currentSlide, setCurrentSlide] = useState(0) + const [highestSlide, setHighestSlide] = useState(0) + + const [prevDisable, setPrevDisable] = useState(true) // previous button disabled? + const [nextDisable, setNextDisable] = useState(true) // next button disabled? + + const [loaded, setLoaded] = useState(false) // use to avoid running logic before db calls return + const [finished, setFinished] = useState(false) + const showId = getPageId() + + /** Component did mount */ + useEffect(() => { + load() + }, []) + + useEffect(() => { + // "Touple" as array of the values + // of [prevDisable, nextDisable, highestSlide] + const state = evaluateState() + setNextDisable(state[1]) + setPrevDisable(state[0]) + const high = state[2] + setHighestSlide(high) + //TODO: Update session with new highestSlide / currentSlide (maybe?) + const finish = high == content.length - 1 + updateSession(window.sessionStorage.getItem('id'), high, finish) + }, [currentSlide]) + + useEffect(() => { + // This will run before the data loads from db this eliminates that run + if (!loaded) return + const visited = currentSlide < highestSlide + const isQuiz = content[currentSlide].isQuiz + const final = currentSlide === content.length - 1 + if (nextDisable && loaded && !visited && !isQuiz) { + window.setTimeout(() => { + setNextDisable(final) + setFinished(final) + }, showTimer) + } + }, [nextDisable]) + + async function load() { + const show = await fetchShow(showId) + if (show.length > 0) { + const content = await fetchSlides(showId) + const session = await fetchSession(showId) + + let current = Number(session.highest) + if (session.complete) { + current = 0 + } + setNoShow(false) + setShowTitle(show.showTitle) + setShowTimer(show.showTimer) + setShowAnimation(show.animation) + setContent(content) + setCurrentSlide(current) + setHighestSlide(Number(session.highest)) + setLoaded(true) + setFinished(session.complete) + setNextDisable(!session.complete) + window.setTimeout(() => { + setNextDisable(false) + }, show.showTimer) + } else { + setLoaded(true) + } + } + + function evaluateState() { + // Handle behavior for next and prev disables + let next = true + let prev = false + let high = highestSlide + if (high < currentSlide) high = currentSlide + const visited = currentSlide < highestSlide + if (finished || visited) { + next = false + } + + // Final global check that will exist despite finished + if (currentSlide == 0) { + // First slide + prev = true + } else if (currentSlide == content.length - 1) { + // Last slide + next = true + } + return [prev, next, high] + } + + function validate() { + // This will be called from subcompents to revalidate the current component + let final = false + if (currentSlide === content.length - 1) final = true + setNextDisable(final) + } + + function _next() { + let next = currentSlide + 1 + if (next == content.length) next = currentSlide // set disable + setCurrentSlide(next) + } + + function _prev() { + let prev = currentSlide - 1 + if (prev < 0) prev = 0 // set state to disable + setCurrentSlide(prev) + } + if (!loaded) return + if (noShow) { + return ( +
    +

    Sorry

    +

    This show is not available.

    +
    + ) + } + return ( +
    +

    {showTitle}

    +

    +
    + +
    + + setCurrentSlide(slide)} + /> + + {isAdmin ? ( + + ) : null} + +
    + ) +} + +Present.propTypes = { + isAdmin: PropTypes.string, +} diff --git a/javascript/Present/PresentView.jsx b/javascript/Present/PresentView.jsx new file mode 100644 index 0000000..0228305 --- /dev/null +++ b/javascript/Present/PresentView.jsx @@ -0,0 +1,44 @@ +'use strict' +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Viewspace from './Viewspace.jsx' +import QuizViewspace from './Quiz/QuizViewspace.jsx' + +export default class PresentView extends Component { + constructor(props) { + super(props) + } + + render() { + if (this.props.content != undefined) { + let viewspace = (this.props.content.isQuiz) ? + () : + + + //background styling check image or color + let backgroundStyle = {minHeight: 500, minWidth: 300, height: '10rem', width: '60rem', backgroundImage: `url(${this.props.content.background})`, backgroundRepeat: 'no-repeat', backgroundSize: 'cover'} + if (this.props.content.background.charAt(0) === '#') { + backgroundStyle = {minHeight: 500, minWidth: 300, height: '10rem', width: '60rem', backgroundColor: this.props.content.background} + } + return ( +
    + {viewspace} +
    + ) + } + return null + } +} + +PresentView.propTypes = { + content: PropTypes.object, + currentSlide: PropTypes.number, + high: PropTypes.number, + validate: PropTypes.func, +} diff --git a/javascript/Present/Quiz/AnswerComponent.jsx b/javascript/Present/Quiz/AnswerComponent.jsx new file mode 100644 index 0000000..bd52c5b --- /dev/null +++ b/javascript/Present/Quiz/AnswerComponent.jsx @@ -0,0 +1,38 @@ +import React from 'react' + +import { Form, Button } from 'react-bootstrap' + +const { Check } = Form + +export default function AnswerComponent(props) { + if (props.quizContent != undefined) { + let a = props.quizContent.answers.map((answer, i) => { + // Shows a check if the answer has been correctly answered before + const c = props.quizContent.correct + let ans = ((props.currentSlide < props.highestSlide || props.finished) && (c.includes(i) || c.includes(i.toString()))) ? + ({answer} ) : answer + return ( +
    + +
    ) + }) + + const selectButton = ( + + ) + return ( +
    + {a} + {props.quizContent.type === 'select' ? selectButton : undefined} +
    + ) + } + return null + } \ No newline at end of file diff --git a/javascript/Present/Quiz/QuizAlert.jsx b/javascript/Present/Quiz/QuizAlert.jsx new file mode 100644 index 0000000..863dabd --- /dev/null +++ b/javascript/Present/Quiz/QuizAlert.jsx @@ -0,0 +1,36 @@ +import React from 'react' +import { Alert } from 'react-bootstrap' +export default function QuizAlert(props) { + let alert = undefined + + const select_index = props.selected[0] + 3 + const right_message = (props.feedback[0] === 'global' || props.qType === 'select') + ? props.feedback[1] : props.feedback[select_index] + const wrong_message = (props.feedback[0] === 'global' || props.qType === 'select') + ? props.feedback[2] : props.feedback[select_index] + switch (props.state) { + case 'correct': + alert = ( + {right_message} + ) + break; + case 'incorrect': + alert = ( + {wrong_message} + ) + break; + case 'partial': + alert = ( + You are partially correct + ) + break; + case 'initial': + default: + alert = Select the correct answer to continue + break; + } + + return ( + {alert} + ) +} \ No newline at end of file diff --git a/javascript/Present/Quiz/QuizViewspace.jsx b/javascript/Present/Quiz/QuizViewspace.jsx new file mode 100644 index 0000000..289e491 --- /dev/null +++ b/javascript/Present/Quiz/QuizViewspace.jsx @@ -0,0 +1,114 @@ +import React, { useState, useEffect } from 'react' + +import AnswersComponent from './AnswerComponent' +import Alert from './QuizAlert' + +export default function QuizViewspace(props) { + + const [selected, setSelected] = useState([]) + const [gradeState, setGradeState] = useState('unchosen') + + useEffect(() => { + setSelected([]) + setGradeState('unchosen') + }, [props.currentSlide]) + + function validate(e) { + let ids = e.target.id.split('-') + let s = [...selected] + let g = 'incorrect' + if (props.quizContent.type === 'choice') { + s = [Number(ids[1])] + if (evaluateChoice(s)) { + g = 'correct' + props.validate() + } + } // Multiple select + else { + if (!s.includes(Number(ids[1]))) { + s.push(Number(ids[1])) + } else { + s = s.filter((i) => i != Number(ids[1])) + } + // Not evaluated until button gets pressed + g = gradeState + } + setGradeState(g) + setSelected(s) + } + + function evaluateChoice(s) { + if (s.length != props.quizContent.correct.length) { + return false + } + for (let value of props.quizContent.correct) { + if (!s.includes(Number(value))) { + return false + } + } + return true + } + + function evaluateSelect(s) { + if (s === undefined) s = [...selected] + let partial = false + let correct = true + for (let value of props.quizContent.correct) { + if (!selected.includes(Number(value))) { + correct = false + } else { + partial = true + } + } + let g = 'unchosen' + if (correct) { + g = 'correct' + props.validate() + } + else if (partial) g = 'partial' + else g = 'incorrect' + setGradeState(g) + } + + + let answersComponent = undefined + let titleComponent = undefined + if (props.quizContent == undefined) { + titleComponent = "Error - Empty Quiz" + answersComponent = (
    +

    This quiz slide has not been filled with data

    +

    If you are a student, contact the one responsible for making you take this

    +

    If you are an admin, fill out this slide with data

    +
    ) + } + else { + answersComponent = + titleComponent = props.quizContent.question + } + + let image = undefined + let align = undefined + + if (props.content.media != undefined) { + image = ( +
    + {props.content.media} +
    + ) + align = props.content.media.align + } + + return ( +
    + {(align === 'left') ? image : undefined} +
    +

    {titleComponent}

    + {answersComponent} + + + +
    + {(align === 'right') ? image : undefined} +
    + ) +} \ No newline at end of file diff --git a/javascript/Present/Viewspace.jsx b/javascript/Present/Viewspace.jsx new file mode 100644 index 0000000..e3aebf5 --- /dev/null +++ b/javascript/Present/Viewspace.jsx @@ -0,0 +1,78 @@ +'use strict' +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import {Editor, EditorState, convertFromRaw} from 'draft-js' + +import decorator from '../Resources/Draft/LinkDecorator.js' +import CustomStyleMap from '../Resources/Draft/CustomStyleMap.js' +import CustomBlockFn from '../Resources/Draft/CustomBlockFn.js'; + +import '../Edit/custom.css' + +export default class Viewspace extends Component { + constructor(props) { + super(props) + this.state = { + editorState: EditorState.createEmpty(decorator) + } + this.loadEditorState = this.loadEditorState.bind(this) + } + + componentDidMount() { + // decode the Editor state from the content field. + if (this.props.content.saveContent != undefined) { + this.loadEditorState(this.props.content) + } + } + + componentDidUpdate(prevProps) { + if (prevProps.content.saveContent != this.props.content.saveContent) { + this.loadEditorState() + } + } + + loadEditorState(content) { + let contentState = convertFromRaw(JSON.parse(this.props.content.saveContent)) + + this.setState({ + editorState: EditorState.createWithContent(contentState, decorator) + }) + } + + render() { + let styles = CustomStyleMap + + if (this.state.editorState != undefined) { + // Custom text color + const color = this.state.editorState.getCurrentInlineStyle().keys().next().value + if (color != undefined) { + const styleObj = JSON.parse('{"' + color + '":{"color":"' + color + '"}}') + styles = Object.assign(styleObj, CustomStyleMap) + } + } + let image = undefined + let align = undefined + if (this.props.content.media != undefined) { + image = ( +
    + {this.props.content.media} +
    + ) + align = this.props.content.media.align + } + return ( +
    + {(align === 'left') ? image : undefined} +
    + +
    + {(align === 'right') ? image : undefined} +
    + + ) + } +} + +Viewspace.propTypes = { + content: PropTypes.object, +} diff --git a/javascript/Present/custom.css b/javascript/Present/custom.css new file mode 100644 index 0000000..8f9913d --- /dev/null +++ b/javascript/Present/custom.css @@ -0,0 +1,14 @@ +.progress { + margin: auto; + max-width: 800px; + margin-bottom: 20px !important; +} +.dropdown-menu { + max-height: 150px; + overflow: auto; +} + +.tipp-butt { + margin: 5px; + text-align: 'center'; +} \ No newline at end of file diff --git a/javascript/Present/index.jsx b/javascript/Present/index.jsx new file mode 100644 index 0000000..882d215 --- /dev/null +++ b/javascript/Present/index.jsx @@ -0,0 +1,10 @@ +'use strict' +import React from 'react' +import ReactDOM from 'react-dom' +import Present from './Present.jsx' + +/* global isAdmin */ +ReactDOM.render( + , + document.getElementById('present') +) diff --git a/javascript/Resources/Components/Skeleton.jsx b/javascript/Resources/Components/Skeleton.jsx new file mode 100644 index 0000000..9dc320a --- /dev/null +++ b/javascript/Resources/Components/Skeleton.jsx @@ -0,0 +1,18 @@ +import React from 'react' +export default function Skeleton(props) { + const title = (props.title === undefined) ? "Loading show..." : props.title + return ( +
    +

    {title}

    +
    + Loading... +
    +
    + ) +} + +const spinner = { + marginRight: 'auto', + marginLeft: 'auto', + marginTop: '10rem', +} \ No newline at end of file diff --git a/javascript/Resources/Decision.js b/javascript/Resources/Decision.js deleted file mode 100644 index 9dcadda..0000000 --- a/javascript/Resources/Decision.js +++ /dev/null @@ -1,10 +0,0 @@ -export default class DecisionResource { - constructor() { - this.id = 0 - this.title = '' - this.message = '' - this.next = true - this.slideId = 0 - this.sorting = 0 - } -} diff --git a/javascript/Resources/Draft/CustomBlockFn.js b/javascript/Resources/Draft/CustomBlockFn.js new file mode 100644 index 0000000..8c115e9 --- /dev/null +++ b/javascript/Resources/Draft/CustomBlockFn.js @@ -0,0 +1,10 @@ +export default function(block) { + // Adds alignment property to the class name of a block + let className = block.getType() + if (block.getData().get('align') != undefined) { + if (block.getType() !== 'unordered-list-item' && block.getType() !== 'ordered-list-item') { + className += " " + block.getData().get('align') + } + } + return className +} \ No newline at end of file diff --git a/javascript/Resources/Draft/CustomStyleMap.js b/javascript/Resources/Draft/CustomStyleMap.js new file mode 100644 index 0000000..6da03c2 --- /dev/null +++ b/javascript/Resources/Draft/CustomStyleMap.js @@ -0,0 +1,28 @@ +const CustomStyleMap = { + "#f44336": {color: '#f44336'}, + "#e91e63": {color: '#e91e63'}, + "#9c27b0": {color: '#9c27b0'}, + "#673ab7": {color: '#673ab7'}, + "#3f51b5": {color: '#3f51b5'}, + "#2196f3": {color: '#2196f3'}, + "#03a9f4": {color: '#03a9f4'}, + "#00bcd4": {color: '#00bcd4'}, + "#009688": {color: '#009688'}, + "#4caf50": {color: '#4caf50'}, + "#8bc34a": {color: '#8bc34a'}, + "#cddc39": {color: '#cddc39'}, + "#ffeb3b": {color: '#ffeb3b'}, + "#ffc107": {color: '#ffc107'}, + "#ff9800": {color: '#ff9800'}, + "#ff5722": {color: '#ff5722'}, + "#795548": {color: '#795548'}, + "#607d8b": {color: '#607d8b'}, + "align-right" : {textAlign: 'right', display: 'inline-block', width: '100%'}, + "align-center" : {textAlign: 'center', display: 'inline-block', width: '100%'}, + "align-left" : {textAlign: 'left', display: 'inline-block', width: '100%'}, + "underline" : {textDecoration: 'underline'}, + "BOLD" : {fontWeight: 'bold'}, + "ITALIC": {fontStyle: 'italic'} +} + +export default CustomStyleMap \ No newline at end of file diff --git a/javascript/Resources/Draft/LinkDecorator.js b/javascript/Resources/Draft/LinkDecorator.js new file mode 100644 index 0000000..4c2d272 --- /dev/null +++ b/javascript/Resources/Draft/LinkDecorator.js @@ -0,0 +1,53 @@ +// Link Decorator +import React from 'react' +import { CompositeDecorator } from 'draft-js' +import Tippy from '@tippyjs/react' +import 'tippy.js/themes/material.css' + +function findLinkEntities(contentBlock, callback, contentState) { + contentBlock.findEntityRanges( + (character) => { + const entityKey = character.getEntity(); + return ( + entityKey !== null && + contentState.getEntity(entityKey).getType() === 'LINK' + ); + }, + callback + ); +} +const Link = (props) => { + let {url} = props.contentState.getEntity(props.entityKey).getData(); + const urlA = url.split("//") + if (urlA[0] != "https:" && urlA[0] != "http:") { + url = "http://" + url + } + return ( + {url}} arrow={true} interactive={true}> + + {props.children} + + + ); +}; + +const linkStyle = { + color: 'royalblue', + textDecorationColor: 'royalblue', + // There is a bug where this disappears when text is aligned to center/right + textDecorationLine: 'underline !important' +} + +const icon = { + fontSize: '50%', + verticalAlign: 'middle' +} + +const decorator = new CompositeDecorator([ + { + strategy: findLinkEntities, + component: Link, + }, +]); + +export default decorator; \ No newline at end of file diff --git a/javascript/Resources/Draft/TextColorMap.js b/javascript/Resources/Draft/TextColorMap.js new file mode 100644 index 0000000..118ca78 --- /dev/null +++ b/javascript/Resources/Draft/TextColorMap.js @@ -0,0 +1,22 @@ +const TextColorMap = { + "#f44336": {color: '#f44336'}, + "#e91e63": {color: '#e91e63'}, + "#9c27b0": {color: '#9c27b0'}, + "#673ab7": {color: '#673ab7'}, + "#3f51b5": {color: '#3f51b5'}, + "#2196f3": {color: '#2196f3'}, + "#03a9f4": {color: '#03a9f4'}, + "#00bcd4": {color: '#00bcd4'}, + "#009688": {color: '#009688'}, + "#4caf50": {color: '#4caf50'}, + "#8bc34a": {color: '#8bc34a'}, + "#cddc39": {color: '#cddc39'}, + "#ffeb3b": {color: '#ffeb3b'}, + "#ffc107": {color: '#ffc107'}, + "#ff9800": {color: '#ff9800'}, + "#ff5722": {color: '#ff5722'}, + "#795548": {color: '#795548'}, + "#607d8b": {color: '#607d8b'}, +}; // If you edit this, make sure to reflect the changes in CustomStyleMap.js + +export default TextColorMap; \ No newline at end of file diff --git a/javascript/Resources/Search.js b/javascript/Resources/Search.js new file mode 100644 index 0000000..768478c --- /dev/null +++ b/javascript/Resources/Search.js @@ -0,0 +1,5 @@ +const fuzzysort = require('fuzzysort') + +export const fuzzySearch = (a, b) => { + return fuzzysort.single(a, b) +} \ No newline at end of file diff --git a/javascript/Resources/Section.js b/javascript/Resources/Section.js deleted file mode 100644 index d01881a..0000000 --- a/javascript/Resources/Section.js +++ /dev/null @@ -1,8 +0,0 @@ -let Section = { - id: 0, - showId: 0, - sorting: 0, - title: '', -} - -export default Section diff --git a/javascript/Resources/Show.js b/javascript/Resources/Show.js index 50ab834..087b6af 100644 --- a/javascript/Resources/Show.js +++ b/javascript/Resources/Show.js @@ -1,6 +1,10 @@ let Show = { - id: 0, - title: '' + id: -1, + active: false, + title: undefined, + content: [], + preview: undefined, + disabled: false } export default Show diff --git a/javascript/Resources/Slide.js b/javascript/Resources/Slide.js deleted file mode 100644 index dce5a11..0000000 --- a/javascript/Resources/Slide.js +++ /dev/null @@ -1,12 +0,0 @@ -export default class SlideObj { - constructor(slideId = 0) { - this.id = slideId - this.delay = 0 - this.sectionId = 0 - this.sorting = 1 - this.title = 'Untitled slide' - this.content = '

    Content here...

    ' - this.backgroundImage = '' - this.decisions = [] - } -} diff --git a/javascript/Resources/dom-to-image.js b/javascript/Resources/dom-to-image.js new file mode 100644 index 0000000..a2e6f8d --- /dev/null +++ b/javascript/Resources/dom-to-image.js @@ -0,0 +1,770 @@ +(function (global) { + 'use strict'; + + var util = newUtil(); + var inliner = newInliner(); + var fontFaces = newFontFaces(); + var images = newImages(); + + // Default impl options + var defaultOptions = { + // Default is to fail on error, no placeholder + imagePlaceholder: undefined, + // Default cache bust is false, it will use the cache + cacheBust: false + }; + + var domtoimage = { + toSvg: toSvg, + toPng: toPng, + toJpeg: toJpeg, + toBlob: toBlob, + toPixelData: toPixelData, + impl: { + fontFaces: fontFaces, + images: images, + util: util, + inliner: inliner, + options: {} + } + }; + + if (typeof module !== 'undefined') + module.exports = domtoimage; + else + global.domtoimage = domtoimage; + + + /** + * @param {Node} node - The DOM Node object to render + * @param {Object} options - Rendering options + * @param {Function} options.filter - Should return true if passed node should be included in the output + * (excluding node means excluding it's children as well). Not called on the root node. + * @param {String} options.bgcolor - color for the background, any valid CSS color value. + * @param {Number} options.width - width to be applied to node before rendering. + * @param {Number} options.height - height to be applied to node before rendering. + * @param {Object} options.style - an object whose properties to be copied to node's style before rendering. + * @param {Number} options.quality - a Number between 0 and 1 indicating image quality (applicable to JPEG only), + defaults to 1.0. + * @param {String} options.imagePlaceholder - dataURL to use as a placeholder for failed images, default behaviour is to fail fast on images we can't fetch + * @param {Boolean} options.cacheBust - set to true to cache bust by appending the time to the request url + * @return {Promise} - A promise that is fulfilled with a SVG image data URL + * */ + function toSvg(node, options) { + options = options || {}; + copyOptions(options); + return Promise.resolve(node) + .then(function (node) { + return cloneNode(node, options.filter, true); + }) + .then(embedFonts) + .then(inlineImages) + .then(applyOptions) + .then(function (clone) { + return makeSvgDataUri(clone, + options.width || util.width(node), + options.height || util.height(node) + ); + }); + + function applyOptions(clone) { + if (options.bgcolor) clone.style.backgroundColor = options.bgcolor; + + if (options.width) clone.style.width = options.width + 'px'; + if (options.height) clone.style.height = options.height + 'px'; + + if (options.style) + Object.keys(options.style).forEach(function (property) { + clone.style[property] = options.style[property]; + }); + + return clone; + } + } + + /** + * @param {Node} node - The DOM Node object to render + * @param {Object} options - Rendering options, @see {@link toSvg} + * @return {Promise} - A promise that is fulfilled with a Uint8Array containing RGBA pixel data. + * */ + function toPixelData(node, options) { + return draw(node, options || {}) + .then(function (canvas) { + return canvas.getContext('2d').getImageData( + 0, + 0, + util.width(node), + util.height(node) + ).data; + }); + } + + /** + * @param {Node} node - The DOM Node object to render + * @param {Object} options - Rendering options, @see {@link toSvg} + * @return {Promise} - A promise that is fulfilled with a PNG image data URL + * */ + function toPng(node, options) { + return draw(node, options || {}) + .then(function (canvas) { + return canvas.toDataURL(); + }); + } + + /** + * @param {Node} node - The DOM Node object to render + * @param {Object} options - Rendering options, @see {@link toSvg} + * @return {Promise} - A promise that is fulfilled with a JPEG image data URL + * */ + function toJpeg(node, options) { + options = options || {}; + return draw(node, options) + .then(function (canvas) { + return canvas.toDataURL('image/jpeg', options.quality || 1.0); + }); + } + + /** + * @param {Node} node - The DOM Node object to render + * @param {Object} options - Rendering options, @see {@link toSvg} + * @return {Promise} - A promise that is fulfilled with a PNG image blob + * */ + function toBlob(node, options) { + return draw(node, options || {}) + .then(util.canvasToBlob); + } + + function copyOptions(options) { + // Copy options to impl options for use in impl + if(typeof(options.imagePlaceholder) === 'undefined') { + domtoimage.impl.options.imagePlaceholder = defaultOptions.imagePlaceholder; + } else { + domtoimage.impl.options.imagePlaceholder = options.imagePlaceholder; + } + + if(typeof(options.cacheBust) === 'undefined') { + domtoimage.impl.options.cacheBust = defaultOptions.cacheBust; + } else { + domtoimage.impl.options.cacheBust = options.cacheBust; + } + } + + function draw(domNode, options) { + return toSvg(domNode, options) + .then(util.makeImage) + .then(util.delay(100)) + .then(function (image) { + var canvas = newCanvas(domNode); + canvas.getContext('2d').drawImage(image, 0, 0); + return canvas; + }); + + function newCanvas(domNode) { + var canvas = document.createElement('canvas'); + canvas.width = options.width || util.width(domNode); + canvas.height = options.height || util.height(domNode); + + if (options.bgcolor) { + var ctx = canvas.getContext('2d'); + ctx.fillStyle = options.bgcolor; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + return canvas; + } + } + + function cloneNode(node, filter, root) { + if (!root && filter && !filter(node)) return Promise.resolve(); + + return Promise.resolve(node) + .then(makeNodeCopy) + .then(function (clone) { + return cloneChildren(node, clone, filter); + }) + .then(function (clone) { + return processClone(node, clone); + }); + + function makeNodeCopy(node) { + if (node instanceof HTMLCanvasElement) return util.makeImage(node.toDataURL()); + return node.cloneNode(false); + } + + function cloneChildren(original, clone, filter) { + var children = original.childNodes; + if (children.length === 0) return Promise.resolve(clone); + + return cloneChildrenInOrder(clone, util.asArray(children), filter) + .then(function () { + return clone; + }); + + function cloneChildrenInOrder(parent, children, filter) { + var done = Promise.resolve(); + children.forEach(function (child) { + done = done + .then(function () { + return cloneNode(child, filter); + }) + .then(function (childClone) { + if (childClone) parent.appendChild(childClone); + }); + }); + return done; + } + } + + function processClone(original, clone) { + if (!(clone instanceof Element)) return clone; + + return Promise.resolve() + .then(cloneStyle) + .then(clonePseudoElements) + .then(copyUserInput) + .then(fixSvg) + .then(function () { + return clone; + }); + + function cloneStyle() { + copyStyle(window.getComputedStyle(original), clone.style); + + function copyStyle(source, target) { + if (source.cssText) target.cssText = source.cssText; + else copyProperties(source, target); + + function copyProperties(source, target) { + util.asArray(source).forEach(function (name) { + target.setProperty( + name, + source.getPropertyValue(name), + source.getPropertyPriority(name) + ); + }); + } + } + } + + function clonePseudoElements() { + [':before', ':after'].forEach(function (element) { + clonePseudoElement(element); + }); + + function clonePseudoElement(element) { + var style = window.getComputedStyle(original, element); + var content = style.getPropertyValue('content'); + + if (content === '' || content === 'none') return; + + var className = util.uid(); + clone.className = clone.className + ' ' + className; + var styleElement = document.createElement('style'); + styleElement.appendChild(formatPseudoElementStyle(className, element, style)); + clone.appendChild(styleElement); + + function formatPseudoElementStyle(className, element, style) { + var selector = '.' + className + ':' + element; + var cssText = style.cssText ? formatCssText(style) : formatCssProperties(style); + return document.createTextNode(selector + '{' + cssText + '}'); + + function formatCssText(style) { + var content = style.getPropertyValue('content'); + return style.cssText + ' content: ' + content + ';'; + } + + function formatCssProperties(style) { + + return util.asArray(style) + .map(formatProperty) + .join('; ') + ';'; + + function formatProperty(name) { + return name + ': ' + + style.getPropertyValue(name) + + (style.getPropertyPriority(name) ? ' !important' : ''); + } + } + } + } + } + + function copyUserInput() { + if (original instanceof HTMLTextAreaElement) clone.innerHTML = original.value; + if (original instanceof HTMLInputElement) clone.setAttribute("value", original.value); + } + + function fixSvg() { + if (!(clone instanceof SVGElement)) return; + clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); + + if (!(clone instanceof SVGRectElement)) return; + ['width', 'height'].forEach(function (attribute) { + var value = clone.getAttribute(attribute); + if (!value) return; + + clone.style.setProperty(attribute, value); + }); + } + } + } + + function embedFonts(node) { + return fontFaces.resolveAll() + .then(function (cssText) { + var styleNode = document.createElement('style'); + node.appendChild(styleNode); + styleNode.appendChild(document.createTextNode(cssText)); + return node; + }); + } + + function inlineImages(node) { + return images.inlineAll(node) + .then(function () { + return node; + }); + } + + function makeSvgDataUri(node, width, height) { + return Promise.resolve(node) + .then(function (node) { + node.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + return new XMLSerializer().serializeToString(node); + }) + .then(util.escapeXhtml) + .then(function (xhtml) { + return '' + xhtml + ''; + }) + .then(function (foreignObject) { + return '' + + foreignObject + ''; + }) + .then(function (svg) { + return 'data:image/svg+xml;charset=utf-8,' + svg; + }); + } + + function newUtil() { + return { + escape: escape, + parseExtension: parseExtension, + mimeType: mimeType, + dataAsUrl: dataAsUrl, + isDataUrl: isDataUrl, + canvasToBlob: canvasToBlob, + resolveUrl: resolveUrl, + getAndEncode: getAndEncode, + uid: uid(), + delay: delay, + asArray: asArray, + escapeXhtml: escapeXhtml, + makeImage: makeImage, + width: width, + height: height + }; + + function mimes() { + /* + * Only WOFF and EOT mime types for fonts are 'real' + * see http://www.iana.org/assignments/media-types/media-types.xhtml + */ + var WOFF = 'application/font-woff'; + var JPEG = 'image/jpeg'; + + return { + 'woff': WOFF, + 'woff2': WOFF, + 'ttf': 'application/font-truetype', + 'eot': 'application/vnd.ms-fontobject', + 'png': 'image/png', + 'jpg': JPEG, + 'jpeg': JPEG, + 'gif': 'image/gif', + 'tiff': 'image/tiff', + 'svg': 'image/svg+xml' + }; + } + + function parseExtension(url) { + var match = /\.([^\.\/]*?)$/g.exec(url); + if (match) return match[1]; + else return ''; + } + + function mimeType(url) { + var extension = parseExtension(url).toLowerCase(); + return mimes()[extension] || ''; + } + + function isDataUrl(url) { + return url.search(/^(data:)/) !== -1; + } + + function toBlob(canvas) { + return new Promise(function (resolve) { + var binaryString = window.atob(canvas.toDataURL().split(',')[1]); + var length = binaryString.length; + var binaryArray = new Uint8Array(length); + + for (var i = 0; i < length; i++) + binaryArray[i] = binaryString.charCodeAt(i); + + resolve(new Blob([binaryArray], { + type: 'image/png' + })); + }); + } + + function canvasToBlob(canvas) { + if (canvas.toBlob) + return new Promise(function (resolve) { + canvas.toBlob(resolve); + }); + + return toBlob(canvas); + } + + function resolveUrl(url, baseUrl) { + var doc = document.implementation.createHTMLDocument(); + var base = doc.createElement('base'); + doc.head.appendChild(base); + var a = doc.createElement('a'); + doc.body.appendChild(a); + base.href = baseUrl; + a.href = url; + return a.href; + } + + function uid() { + var index = 0; + + return function () { + return 'u' + fourRandomChars() + index++; + + function fourRandomChars() { + /* see http://stackoverflow.com/a/6248722/2519373 */ + return ('0000' + (Math.random() * Math.pow(36, 4) << 0).toString(36)).slice(-4); + } + }; + } + + function makeImage(uri) { + return new Promise(function (resolve, reject) { + var image = new Image(); + image.onload = function () { + resolve(image); + }; + image.onerror = reject; + image.src = uri; + }); + } + + function getAndEncode(url) { + var TIMEOUT = 30000; + if(domtoimage.impl.options.cacheBust) { + // Cache bypass so we dont have CORS issues with cached images + // Source: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache + url += ((/\?/).test(url) ? "&" : "?") + (new Date()).getTime(); + } + + return new Promise(function (resolve) { + var request = new XMLHttpRequest(); + + request.onreadystatechange = done; + request.ontimeout = timeout; + request.responseType = 'blob'; + request.timeout = TIMEOUT; + request.open('GET', url, true); + request.send(); + + var placeholder; + if(domtoimage.impl.options.imagePlaceholder) { + var split = domtoimage.impl.options.imagePlaceholder.split(/,/); + if(split && split[1]) { + placeholder = split[1]; + } + } + + function done() { + if (request.readyState !== 4) return; + + if (request.status !== 200) { + if(placeholder) { + resolve(placeholder); + } else { + fail('cannot fetch resource: ' + url + ', status: ' + request.status); + } + + return; + } + + var encoder = new FileReader(); + encoder.onloadend = function () { + var content = encoder.result.split(/,/)[1]; + resolve(content); + }; + encoder.readAsDataURL(request.response); + } + + function timeout() { + if(placeholder) { + resolve(placeholder); + } else { + fail('timeout of ' + TIMEOUT + 'ms occured while fetching resource: ' + url); + } + } + + function fail(message) { + console.error(message); + resolve(''); + } + }); + } + + function dataAsUrl(content, type) { + return 'data:' + type + ';base64,' + content; + } + + function escape(string) { + return string.replace(/([.*+?^${}()|\[\]\/\\])/g, '\\$1'); + } + + function delay(ms) { + return function (arg) { + return new Promise(function (resolve) { + setTimeout(function () { + resolve(arg); + }, ms); + }); + }; + } + + function asArray(arrayLike) { + var array = []; + var length = arrayLike.length; + for (var i = 0; i < length; i++) array.push(arrayLike[i]); + return array; + } + + function escapeXhtml(string) { + return string.replace(/#/g, '%23').replace(/\n/g, '%0A'); + } + + function width(node) { + var leftBorder = px(node, 'border-left-width'); + var rightBorder = px(node, 'border-right-width'); + return node.scrollWidth + leftBorder + rightBorder; + } + + function height(node) { + var topBorder = px(node, 'border-top-width'); + var bottomBorder = px(node, 'border-bottom-width'); + return node.scrollHeight + topBorder + bottomBorder; + } + + function px(node, styleProperty) { + var value = window.getComputedStyle(node).getPropertyValue(styleProperty); + return parseFloat(value.replace('px', '')); + } + } + + function newInliner() { + var URL_REGEX = /url\(['"]?([^'"]+?)['"]?\)/g; + + return { + inlineAll: inlineAll, + shouldProcess: shouldProcess, + impl: { + readUrls: readUrls, + inline: inline + } + }; + + function shouldProcess(string) { + return string.search(URL_REGEX) !== -1; + } + + function readUrls(string) { + var result = []; + var match; + while ((match = URL_REGEX.exec(string)) !== null) { + result.push(match[1]); + } + return result.filter(function (url) { + return !util.isDataUrl(url); + }); + } + + function inline(string, url, baseUrl, get) { + return Promise.resolve(url) + .then(function (url) { + return baseUrl ? util.resolveUrl(url, baseUrl) : url; + }) + .then(get || util.getAndEncode) + .then(function (data) { + return util.dataAsUrl(data, util.mimeType(url)); + }) + .then(function (dataUrl) { + return string.replace(urlAsRegex(url), '$1' + dataUrl + '$3'); + }); + + function urlAsRegex(url) { + return new RegExp('(url\\([\'"]?)(' + util.escape(url) + ')([\'"]?\\))', 'g'); + } + } + + function inlineAll(string, baseUrl, get) { + if (nothingToInline()) return Promise.resolve(string); + + return Promise.resolve(string) + .then(readUrls) + .then(function (urls) { + var done = Promise.resolve(string); + urls.forEach(function (url) { + done = done.then(function (string) { + return inline(string, url, baseUrl, get); + }); + }); + return done; + }); + + function nothingToInline() { + return !shouldProcess(string); + } + } + } + + function newFontFaces() { + return { + resolveAll: resolveAll, + impl: { + readAll: readAll + } + }; + + function resolveAll() { + return readAll(document) + .then(function (webFonts) { + return Promise.all( + webFonts.map(function (webFont) { + return webFont.resolve(); + }) + ); + }) + .then(function (cssStrings) { + return cssStrings.join('\n'); + }); + } + + function readAll() { + return Promise.resolve(util.asArray(document.styleSheets)) + .then(getCssRules) + .then(selectWebFontRules) + .then(function (rules) { + return rules.map(newWebFont); + }); + + function selectWebFontRules(cssRules) { + return cssRules + .filter(function (rule) { + return rule.type === CSSRule.FONT_FACE_RULE; + }) + .filter(function (rule) { + return inliner.shouldProcess(rule.style.getPropertyValue('src')); + }); + } + + function getCssRules(styleSheets) { + var cssRules = []; + styleSheets.forEach(function (sheet) { + try { + util.asArray(sheet.cssRules || []).forEach(cssRules.push.bind(cssRules)); + } catch (e) { + // Google's css doesn't like this -> which throws this error everytime + //console.log('Error while reading CSS rules from ' + sheet.href, e.toString()); + } + }); + return cssRules; + } + + function newWebFont(webFontRule) { + return { + resolve: function resolve() { + var baseUrl = (webFontRule.parentStyleSheet || {}).href; + return inliner.inlineAll(webFontRule.cssText, baseUrl); + }, + src: function () { + return webFontRule.style.getPropertyValue('src'); + } + }; + } + } + } + + function newImages() { + return { + inlineAll: inlineAll, + impl: { + newImage: newImage + } + }; + + function newImage(element) { + return { + inline: inline + }; + + function inline(get) { + if (util.isDataUrl(element.src)) return Promise.resolve(); + + return Promise.resolve(element.src) + .then(get || util.getAndEncode) + .then(function (data) { + return util.dataAsUrl(data, util.mimeType(element.src)); + }) + .then(function (dataUrl) { + return new Promise(function (resolve, reject) { + element.onload = resolve; + element.onerror = reject; + element.src = dataUrl; + }); + }); + } + } + + function inlineAll(node) { + if (!(node instanceof Element)) return Promise.resolve(node); + + return inlineBackground(node) + .then(function () { + if (node instanceof HTMLImageElement) + return newImage(node).inline(); + else + return Promise.all( + util.asArray(node.childNodes).map(function (child) { + return inlineAll(child); + }) + ); + }); + + function inlineBackground(node) { + var background = node.style.getPropertyValue('background'); + + if (!background) return Promise.resolve(node); + + return inliner.inlineAll(background) + .then(function (inlined) { + node.style.setProperty( + 'background', + inlined, + node.style.getPropertyPriority('background') + ); + }) + .then(function () { + return node; + }); + } + } + } +})(this); diff --git a/javascript/Section/Listing.jsx b/javascript/Section/Listing.jsx deleted file mode 100644 index ea98281..0000000 --- a/javascript/Section/Listing.jsx +++ /dev/null @@ -1,55 +0,0 @@ -'use strict' -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import SortableList from './SortableList.jsx' -import './slide.css' - -/* global $ */ - -export default class Listing extends Component { - constructor(props) { - super(props) - this.state = {} - this.sortEnd = this.sortEnd.bind(this) - } - - sortEnd(movement) { - const {oldIndex, newIndex,} = movement - const newPosition = this.props.slides[newIndex].sorting - const movingSlideId = this.props.slides[oldIndex].id - $.ajax({ - url: './slideshow/Slide/' + movingSlideId + '/move', - data: { - sectionId: this.props.sectionId, - varname: 'move', - newPosition: newPosition - }, - success: function () { - this.props.load() - }.bind(this), - dataType: 'json', - type: 'patch' - }) - } - - render() { - return ( -
    - -
    - ) - } -} - -Listing.propTypes = { - slides: PropTypes.array, - deleteSlide: PropTypes.func, - load: PropTypes.func, - sectionId: PropTypes.number, -} diff --git a/javascript/Section/Section.jsx b/javascript/Section/Section.jsx deleted file mode 100644 index 93a3baa..0000000 --- a/javascript/Section/Section.jsx +++ /dev/null @@ -1,80 +0,0 @@ -'use strict' -import React, {Component} from 'react' -import Waiting from '../AddOn/Html/Waiting.jsx' -import Listing from './Listing.jsx' - -/* global $, sectionId, slide */ - -export default class Slides extends Component { - constructor() { - super() - this.state = { - loading: false, - slides: [], - } - - this.load = this.load.bind(this) - this.deleteSlide = this.deleteSlide.bind(this) - } - - componentDidMount() { - this.load() - this.initializeSlideLink() - } - - initializeSlideLink() { - $('#add-slide').click(function () { - const response = this.createSlideAjax() - response.success(function () { - this.load() - }.bind(this)) - }.bind(this)) - } - - load() { - this.setState({loading: true}) - $.getJSON('./slideshow/Section/' + sectionId).done(function (data) { - this.setState({loading: false, slides: data,}) - }.bind(this)) - } - - createSlideAjax() { - return $.ajax({ - url: 'slideshow/Slide', - data: { - sectionId: sectionId - }, - dataType: 'json', - type: 'post', - }) - } - - deleteSlide(slideKey) { - const slide = this.state.slides[slideKey] - $.ajax({ - url: 'slideshow/Slide/' + slide.id, - dataType: 'json', - type: 'delete', - success: function () { - this.load() - }.bind(this), - error: function () {}.bind(this) - }) - } - - listSlides() { - if (this.state.loading) { - return - } else { - return - } - } - - render() { - return ( -
    - {this.listSlides()} -
    - ) - } -} diff --git a/javascript/Section/SlideRow.jsx b/javascript/Section/SlideRow.jsx deleted file mode 100644 index 7346522..0000000 --- a/javascript/Section/SlideRow.jsx +++ /dev/null @@ -1,42 +0,0 @@ -'use strict' -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import {SortableHandle} from 'react-sortable-hoc' - -export default class SlideRow extends Component { - constructor(props) { - super(props) - this.state = {} - } - - render() { - const {slideKey, value, deleteSlide} = this.props - const DragHandle = SortableHandle(() => DragTag()) - return ( -
  • - Slide {value.sorting}: {value.title} -
    - - - - - -
    -
  • - ) - } -} - -SlideRow.propTypes = { - value: PropTypes.object, - deleteSlide: PropTypes.func, - slideKey: PropTypes.number, -} - -const DragTag = () => { - return ( - - - - ) -} diff --git a/javascript/Section/SortableItem.jsx b/javascript/Section/SortableItem.jsx deleted file mode 100644 index a026050..0000000 --- a/javascript/Section/SortableItem.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react' -import {SortableElement} from 'react-sortable-hoc' -import SlideRow from './SlideRow.jsx' - -const SortableItem = SortableElement(({slideKey, value, deleteSlide, load}) => { - return () -}) - -export default SortableItem diff --git a/javascript/Section/SortableList.jsx b/javascript/Section/SortableList.jsx deleted file mode 100644 index 3563889..0000000 --- a/javascript/Section/SortableList.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react' -import {SortableContainer} from 'react-sortable-hoc' -import SortableItem from './SortableItem.jsx' - - -const SortableList = SortableContainer(({ - listing, - deleteSlide, - load -}) => { - let slides = listing.map(function (value, key) { - return () - }) - - return ( -
      - {slides} -
    - ) -}) - - -export default SortableList diff --git a/javascript/Section/index.jsx b/javascript/Section/index.jsx deleted file mode 100644 index 82c1400..0000000 --- a/javascript/Section/index.jsx +++ /dev/null @@ -1,6 +0,0 @@ -'use strict' -import React from 'react' -import ReactDOM from 'react-dom' -import Section from './Section.jsx' - -ReactDOM.render(
    , document.getElementById('Section')) diff --git a/javascript/Section/slide.css b/javascript/Section/slide.css deleted file mode 100644 index bd35201..0000000 --- a/javascript/Section/slide.css +++ /dev/null @@ -1,31 +0,0 @@ -.sortableHelper { - list-style-type: none; - border : 1px dashed black; - padding: 5px; -} - -.sortableHelper .drag:hover { - cursor: grabbing; - cursor: -webkit-grabbing; -} - -.drag { - border-radius : 4px; - border : 1px solid #777; - display: inline-block; - padding: 6px 12px; - font-size : 14px; - line-height : 20px; - margin: 0px 3px 0px 0px; - text-align : center; - vertical-align : middle; -} - -.drag:hover { - cursor: grab; - cursor: -webkit-grab; -} - -.options .btn { - margin-right : 3px; -} diff --git a/javascript/SectionForm/Form.jsx b/javascript/SectionForm/Form.jsx deleted file mode 100644 index 4f65ad7..0000000 --- a/javascript/SectionForm/Form.jsx +++ /dev/null @@ -1,58 +0,0 @@ -'use strict' -import React from 'react' -import InputField from '../AddOn/Form/InputField.jsx' -import Section from '../Resources/Section.js' -import Abstract from '../AddOn/Mixin/Abstract.jsx' -import PropTypes from 'prop-types' - -/* global $, showId */ - -export default class SectionForm extends Abstract { - constructor(props) { - super(props) - Section.showId = showId - this.state = { - resource: Section, - errors: { - title: false - }, - } - this.save = this.save.bind(this) - } - - save() { - $.ajax({ - url: './slideshow/Section', - data: this.state.resource, - dataType: 'json', - type: 'post', - success: function () { - this.props.success() - }.bind(this), - error: function () {}.bind(this) - }) - } - - render() { - return ( -
    -
    - - - -
    - ) - } -} - -SectionForm.propTypes = { - success: PropTypes.func.isRequired -} diff --git a/javascript/SectionForm/Section.jsx b/javascript/SectionForm/Section.jsx deleted file mode 100644 index 9c5632d..0000000 --- a/javascript/SectionForm/Section.jsx +++ /dev/null @@ -1,42 +0,0 @@ -'use strict' -import React, {Component} from 'react' -import Modal from '../AddOn/Html/Modal.jsx' -import Form from './Form.jsx' - -/* global $, openStatus, slide, showId */ - -export default class Section extends Component { - constructor(props) { - super(props) - this.state = {showForm:false} - this.initializeSectionLink() - this.showForm = this.showForm.bind(this) - this.hideForm = this.hideForm.bind(this) - } - - initializeSectionLink() { - $('#add-section').click(function () { - this.showForm() - }.bind(this)) - } - - sectionSaved() { - window.location.href = './slideshow/Show/' + showId - } - - showForm() { - this.setState({showForm: true}) - openStatus = false - slide() - } - - hideForm() { - this.setState({showForm: false}) - } - - render() { - return ( -
    - ) - } -} diff --git a/javascript/Session/SessionTable.jsx b/javascript/Session/SessionTable.jsx new file mode 100644 index 0000000..8bc1606 --- /dev/null +++ b/javascript/Session/SessionTable.jsx @@ -0,0 +1,170 @@ +'use strict' +import React, {Component} from 'react' +import {Table} from 'react-bootstrap' +import './custom.css' + +/* global $ */ + +export default class SessionTable extends Component { + constructor(props) { + super(props) + + this.state = { + sessionFlag: null, + sortStatus: true, + sortName: false, + } + this.getSessionInfo = this.getSessionInfo.bind(this) + this.sortStatus = this.sortStatus.bind(this) + this.compareStatus = this.compareStatus.bind(this) + this.sortUsername = this.sortUsername.bind(this) + this.compareUsername = this.compareUsername.bind(this) + } + + componentDidMount() { + this.getSessionInfo() + } + + getSessionInfo() { + $.ajax({ + url: './slideshow/Session/all?id=' + window.sessionStorage.getItem('id'), + type: 'GET', + dataType: 'json', + success: function (data) { + this.setState({showData: data}, () => this.sortStatus()) + }.bind(this), + }) + } + + // sorts data either completed to not started or not started to completed + sortStatus() { + let sortData = [...this.state.showData] + sortData.sort((b, a) => { + return this.compareStatus(b, a) + }) + this.setState({showData: sortData, sortStatus: !this.state.sortStatus}) + } + + compareStatus(b, a) { + if (this.state.sortStatus) { + let x = b + b = a + a = x + } + // order logic + if (Number(a.highestSlide) == 0) { + return 1 //a is not Started + } else if (a.completed == 1) { + return -1 //a is completed + } else { + //a is in progress + if (b.completed == 1) { + return 1 + } else if (b.highestSlide == 0) { + return -1 + } else { + return 0 + } + } + } + + sortUsername() { + let sortData = [...this.state.showData] + sortData.sort((a, b) => { + return this.compareUsername(a, b) + }) + this.setState({showData: sortData, sortName: !this.state.sortName}) + } + + compareUsername(a, b) { + if (this.state.sortName) { + let x = b + b = a + a = x + } + //logic + if (a.username < b.username) { + return -1 + } else if (a.username > b.username) { + return 1 + } else { + return 0 + } + } + + render() { + let tableData = undefined + let status = undefined + if (this.state.showData != null) { + tableData = this.state.showData.map((row) => { + if (Number(row.highestSlide) == 0) { + status = ( + +
    + + Not started +
    + + ) + } else if (Number(row.completed)) { + status = ( + +
    + + Complete +
    + + ) + } else if (Number(row.highestSlide) > 0) { + status = ( + +
    + + In progress +
    + + ) + } + return ( + + + {row.username} + + {status} + + ) + }) + } + + return ( +
    +

    + {window.sessionStorage.getItem('title')} +

    + + + + + + + + {tableData} +
    +
    + Username +
    +
    +
    + Status +
    +
    +
    + ) + } +} diff --git a/javascript/Session/custom.css b/javascript/Session/custom.css new file mode 100644 index 0000000..df17756 --- /dev/null +++ b/javascript/Session/custom.css @@ -0,0 +1,3 @@ +.status-arrows { + cursor: pointer; +} diff --git a/javascript/Session/index.jsx b/javascript/Session/index.jsx new file mode 100644 index 0000000..e2225e7 --- /dev/null +++ b/javascript/Session/index.jsx @@ -0,0 +1,6 @@ +'use strict' +import React from 'react' +import ReactDOM from 'react-dom' +import SessionTable from './SessionTable.jsx' + +ReactDOM.render(, document.getElementById('session')) diff --git a/javascript/Show/DeleteShowTool.jsx b/javascript/Show/DeleteShowTool.jsx new file mode 100644 index 0000000..f2f6dde --- /dev/null +++ b/javascript/Show/DeleteShowTool.jsx @@ -0,0 +1,43 @@ +import React, {useState} from 'react' +import PropTypes from 'prop-types' +import Tippy from '@tippyjs/react' +import 'tippy.js/themes/light-border.css' + +export default function DeleteShowTool(props) { + const [deleteAlert, setDeleteAlert] = useState(false) + + return ( + +
    + + +
    + + } + visible={deleteAlert} + interactive={true}> + Delete this show} + arrow={false}> + + +
    + ) +} + +DeleteShowTool.propTypes = { + delete: PropTypes.func, +} diff --git a/javascript/Show/PreviewUpload.jsx b/javascript/Show/PreviewUpload.jsx new file mode 100644 index 0000000..b6ae20a --- /dev/null +++ b/javascript/Show/PreviewUpload.jsx @@ -0,0 +1,131 @@ +'use strict' +import React, {useState} from 'react' +import {Modal} from 'react-bootstrap' +import Tippy from '@tippyjs/react' +import Dropzone from 'react-dropzone-uploader' +import 'react-dropzone-uploader/dist/styles.css' +import 'tippy.js/themes/light-border.css' +import PropTypes from 'prop-types' + +Media.propTypes = { + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + changePreview: PropTypes.func, + useThumb: PropTypes.func, +} + +/* global $ */ + +export default function Media(props) { + // This is an example of a hook. Check it out on React's documentation site; they're neat + const [modalView, setModalView] = useState(false) + + function insertMedia(fileWithMeta) { + // Handle AJAX + let fMeta = fileWithMeta[0] + let formData = new FormData() + formData.append('media', fMeta.file) + + $.ajax({ + url: `./slideshow/Show/preview?id=${props.id}`, + type: 'POST', + data: formData, + processData: false, + contentType: false, + success: (imageLocation) => { + props.changePreview(JSON.parse(imageLocation)) + }, + error: (req, res) => { + console.log(req) + console.error(res.toString()) + }, + }) + } + + function removeMedia() { + $.ajax({ + url: `./slideshow/Show/${props.id}`, + type: 'DELETE', + data: {type: 'preview'}, + success: (res) => { + if (res === 'true') { + props.changePreview() + } + }, + error: (req, res) => { + console.log(req) + console.error(res.toString()) + }, + }) + } + + function validate({meta}) { + if (meta.status === 'rejected_file_type') { + alert('Sorry, this file type is not supported') + } + } + + return ( + + setModalView(false)} + restoreFocus={false}> + + +
    Show Preview Image
    +
    +
    + +
    +
    Upload New Preview
    + { + insertMedia(fileWithMeta) + setModalView(false) + }} + submitButtonContent={'Insert'} + inputContent={''} + classNames={{ + submitButton: 'btn btn-secondary btn-block drop', + dropzone: 'drop', + }} + /> +
    +
    +
    + + +
    +
    +
    + Change show preview image} + arrow={true}> + + +
    + ) +} diff --git a/javascript/Show/SessionTool.jsx b/javascript/Show/SessionTool.jsx new file mode 100644 index 0000000..478bceb --- /dev/null +++ b/javascript/Show/SessionTool.jsx @@ -0,0 +1,20 @@ +import React from 'react' +import Tippy from '@tippyjs/react' +import 'tippy.js/themes/light-border.css' +import PropTypes from 'prop-types' +SessionTool.propTypes = { + sessionTransition: PropTypes.func, +} + +export default function SessionTool(props) { + return ( + View user progress} + arrow={true}> + + + ) +} diff --git a/javascript/Show/ShowCard.jsx b/javascript/Show/ShowCard.jsx new file mode 100644 index 0000000..76c1c00 --- /dev/null +++ b/javascript/Show/ShowCard.jsx @@ -0,0 +1,285 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' + +import ShowLogo from '../../img/showimg.png' +import PreviewUpload from './PreviewUpload' +import SessionTool from './SessionTool' +import DeleteShowTool from './DeleteShowTool' +import Tippy from '@tippyjs/react' + +import './custom.css' +import 'tippy.js/themes/light-border.css' + +/* global $ */ +export default class ShowCard extends Component { + constructor(props) { + super(props) + + this.state = { + id: -1, + title: null, + img: ShowLogo, + active: 0, + edit: false, + useThumb: false, + } + this.handleSave = this.handleSave.bind(this) + this.deleteShow = this.deleteShow.bind(this) + this.editTitle = this.editTitle.bind(this) + this.updateTitle = this.updateTitle.bind(this) + this.handleActivation = this.handleActivation.bind(this) + this.editTransition = this.editTransition.bind(this) + this.presentTransition = this.presentTransition.bind(this) + this.sessionTransition = this.sessionTransition.bind(this) + this.changePreview = this.changePreview.bind(this) + this.useThumb = this.useThumb.bind(this) + this.submitOnEnter = this.submitOnEnter.bind(this) + } + + componentDidMount() { + this.setState({ + title: this.props.title, + active: Number(this.props.active), + id: this.props.id, + img: this.props.img.length > 0 ? this.props.img : ShowLogo, + }) + } + + handleSave() { + $.ajax({ + url: './slideshow/Show/' + this.state.id, + data: {title: this.state.title, active: this.state.active}, + type: 'put', + dataType: 'json', + success: function () { + this.setState({edit: false}) + this.props.load() + }.bind(this), + error: function (req, err) { + alert('Failed to save data.') + console.error(req, err.toString()) + }.bind(this), + }) + } + + deleteShow() { + $.ajax({ + url: './slideshow/Quiz/' + this.state.id, + type: 'delete', + dataType: 'json', + data: {type: 'all'}, + success: (res) => { + console.log(res) + }, + error: (req, res) => { + console.error(res) + }, + }) + $.ajax({ + url: './slideshow/Slide/' + this.state.id, + type: 'delete', + dataType: 'json', + data: {type: 'all'}, + error: (req, res) => { + console.log('Error Deleting Slides') + console.error(req, res.toString()) + }, + }) + $.ajax({ + url: './slideshow/Show/' + this.state.id, + type: 'delete', + dataType: 'json', + data: {type: 'show'}, + success: function () { + this.props.load() + }.bind(this), + error: function (req, err) { + alert('Failed to delete data.') + console.error(req, err.toString()) + }.bind(this), + }) + } + + editTitle() { + this.setState({edit: true}) + } + + updateTitle(event) { + this.setState({ + title: event.target.value, + }) + } + + handleActivation() { + if (this.state.active > 0) { + this.setState({active: 0}, () => { + this.handleSave() + }) + } else { + this.setState({active: 1}, () => { + this.handleSave() + }) + } + } + + async editTransition() { + await window.sessionStorage.setItem('id', this.state.id) + window.location.href = './slideshow/Slide/Edit/' + } + + async presentTransition() { + await window.sessionStorage.setItem('id', this.state.id) + window.location.href = './slideshow/Slide/Present/' + } + + async sessionTransition() { + await window.sessionStorage.setItem('id', this.state.id) + window.sessionStorage.setItem('title', this.state.title) + window.location.href = './slideshow/Session/table' + } + + changePreview(imgPath) { + if (imgPath == undefined) { + imgPath = ShowLogo + } + this.setState({img: imgPath}) + } + + useThumb(enable) { + $.ajax({ + url: `./slideshow/Show/useThumb?id=${this.state.id}`, + type: 'POST', + data: {value: enable}, + success: (thumb) => { + if (thumb.length > 0) { + this.changePreview(JSON.parse(thumb)) + } + }, + error: (req, res) => { + console.log(req) + console.error(res) + }, + }) + } + + submitOnEnter(event) { + if (event.key === 'Enter') { + this.handleSave() + } + } + + render() { + let cardTitle + if (this.state.edit) { + cardTitle = ( +
    + +
    + +
    +
    + ) + } else { + cardTitle = ( +
    + {this.state.title} + + + +
    + ) + } + + let activeLabel = this.state.active !== 0 ? 'Active' : 'Inactive' + let activeBtnType = + this.state.active !== 0 + ? 'btn btn-outline-success' + : 'btn btn-outline-danger' + + if (this.props.disabled) return null + return ( +
    +
    +
    + {'An +
    +
    +
    +
    +
    + {cardTitle} +
    +
    +
    +
    + + + +
    +
    +
    + + Activate for students
    } + theme="light-border" + arrow={false} + placement="bottom"> + + + +
    +
    +
    + + ) + } +} + +ShowCard.propTypes = { + id: PropTypes.number, + title: PropTypes.string, + active: PropTypes.number, + img: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), + load: PropTypes.func, + disabled: PropTypes.bool, +} diff --git a/javascript/Show/ShowView.jsx b/javascript/Show/ShowView.jsx new file mode 100644 index 0000000..d23ebcc --- /dev/null +++ b/javascript/Show/ShowView.jsx @@ -0,0 +1,357 @@ +'use strict' +import React, {Component} from 'react' +import ShowCard from './ShowCard.jsx' +import Show from '../Resources/Show.js' +import './custom.css' +import {Modal} from 'react-bootstrap' +import {fuzzySearch} from '../Resources/Search.js' + +/* global $ */ + +export default class ShowView extends Component { + constructor() { + super() + this.state = { + resource: Show, + showData: null, + modalOpen: false, + newShowFocus: false, + alphaFilter: true, + activeFilter: false, + newFilter: false, + inv: ['a-z', ''], //last modified is the first spot, second index is last click + loaded: false, + } + + this.getData = this.getData.bind(this) + this.saveNewShow = this.saveNewShow.bind(this) + this.toggleNewSlide = this.toggleNewSlide.bind(this) + this.updateTitle = this.updateTitle.bind(this) + this.handleKeyDown = this._handleKeyDown.bind(this) + this.sortShow = this.sortShow.bind(this) + this.handleActive = this.handleActive.bind(this) + this.handleAlpha = this.handleAlpha.bind(this) + this.handleNew = this.handleNew.bind(this) + this.searchTitle = this.searchTitle.bind(this) + } + + componentDidMount() { + this.getData() + } + + componentDidUpdate() { + if (this.state.inv[1] != '') { + this.sortShow() + } + } + + saveNewShow() { + if (this.state.resource.title != undefined) { + // new show + $.ajax({ + url: './slideshow/Show', + data: this.state.resource, + type: 'post', + dataType: 'json', + success: function (showId) { + window.sessionStorage.setItem('id', showId) + window.location.href = './slideshow/Slide/Edit/' + showId + }.bind(this), + error: function (req, err) { + alert('Failed to save data.') + console.error(req, err.toString()) + }.bind(this), + }) + } else { + alert('Title cannot be empty') + } + } + + toggleNewSlide() { + this.setState({modalOpen: !this.state.modalOpen}) + } + + updateTitle(event) { + let r = this.state.resource + r.title = event.target.value + this.setState({ + resource: r, + }) + } + + /** + * Pulls all the shows from the back-end + */ + getData() { + $.ajax({ + url: './slideshow/Show', + type: 'GET', + dataType: 'json', + success: function (data) { + let inver = this.state.loaded ? 'active' : 'a-z' + if (this.state.loaded) { + if (this.state.inv[0] === 'active') { + inver = 'active' + } else if (this.state.inv[0] === 'inactive') { + inver = 'inactive' + } + } + this.setState( + {showData: data['listing'], loaded: true, inv: [inver, '']}, + () => this.sortShow() + ) + }.bind(this), + error: function (req, err) { + alert('Failed to grab data') + console.error(req, err.toString()) + }.bind(this), + }) + } + + _handleKeyDown(event) { + if (event.key === 'Enter') { + this.saveNewShow() + } + } + + sortShow() { + let showD = [...this.state.showData] + if (this.state.inv[0] !== this.state.inv[1]) { + if (this.state.inv[1] === 'newest') { + showD.sort((a, b) => b.id - a.id) + } else if (this.state.inv[1] === 'a-z') { + showD.sort((a, b) => a.title.localeCompare(b.title)) + } else if ( + this.state.inv[1] === 'active' || + this.state.inv[0] === 'active' + ) { + showD.sort((a, b) => b.active - a.active) + } else if (this.state.inv[0] === 'inactive') { + showD.sort((a, b) => a.active - b.active) + } + this.setState({inv: [this.state.inv[1], '']}) + } else { + if (this.state.inv[0] === 'oldest') { + showD.sort((a, b) => a.id - b.id) + } else if (this.state.inv[0] === 'z-a') { + showD.sort((a, b) => b.title.localeCompare(a.title)) + } else if (this.state.inv[0] === 'inactive') { + showD.sort((a, b) => a.active - b.active) + } + this.setState({ + newFilter: false, + activeFilter: false, + alphaFilter: false, + inv: [this.state.inv[0], ''], + }) + } + this.setState({showData: showD}) + } + + handleAlpha(event) { + if (event.target.id !== 'a-z') { + this.setState({ + alphaFilter: true, + newFilter: false, + activeFilter: false, + inv: [this.state.inv[0], 'a-z'], + }) + } else { + this.setState({ + alphaFilter: true, + newFilter: false, + activeFilter: false, + inv: ['z-a', 'z-a'], + }) + } + } + + handleActive(event) { + if (event.target.id === 'inactive') { + this.setState({ + alphaFilter: false, + newFilter: false, + activeFilter: true, + inv: [this.state.inv[0], 'active'], + }) + } else { + this.setState({ + alphaFilter: false, + newFilter: false, + activeFilter: true, + inv: ['inactive', 'inactive'], + }) + } + } + + handleNew(event) { + if (event.target.id !== 'newest') { + this.setState({ + alphaFilter: false, + newFilter: true, + activeFilter: false, + inv: [this.state.inv[0], 'newest'], + }) + } else { + this.setState({ + alphaFilter: false, + newFilter: true, + activeFilter: false, + inv: ['oldest', 'oldest'], + }) + } + } + + searchTitle(event) { + let showD = [...this.state.showData] + //a is user typed search value + let a = event.target.value.toUpperCase() + for (let f = 0; f < showD.length; f++) { + showD[f].disabled = true + let b = showD[f].title.toUpperCase() + + const weight = fuzzySearch(a, b) + if (weight != null && weight.score >= -60) { + showD[f].disabled = false + } else if (a.length == 0) { + showD[f].disabled = false + } else if (a.length == 1 && weight != null && weight.score >= -70) { + showD[f].disabled = false + } + } + this.setState({showData: showD}) + } + + render() { + const modal = ( + + + New Show + + + +

    + +
    +
    + ) + + if (this.state.showData === null) { + return
    + } else { + let cards = this.state.showData.map( + function (show) { + return ( + + ) + }.bind(this) + ) + + let dropDownItems = ( +
    + + + +
    + ) + let cardStyle = {border: 'solid 3px white', color: 'dimgrey'} + if (this.state.newShowFocus) { + cardStyle = { + border: 'solid 3px #337ab7', + color: '#337ab7', + cursor: 'pointer', + } + } + return ( +
    +

    Shows

    +
    +
    +
    + +
    +
    +
    +
    + +
    + {dropDownItems} +
    +
    +
    +
    +
    +
    + {cards} +
    +
    +
    +
    this.setState({newShowFocus: true})} + onMouseLeave={() => this.setState({newShowFocus: false})} + style={cardStyle}> +
    +
    Create New Show
    + +
    +
    +
    +
    + {modal} +
    + ) + } + } +} diff --git a/javascript/Show/custom.css b/javascript/Show/custom.css new file mode 100644 index 0000000..78174a7 --- /dev/null +++ b/javascript/Show/custom.css @@ -0,0 +1,70 @@ +.card-img-caption { + border-top-left-radius: calc(.25rem - 1px); + border-top-right-radius: calc(.25rem - 1px); +} + +.card-img-top { + z-index: 0; + max-width: 300px; + max-height: 250px; + min-width: 300px; + min-height: 250px; +} + +.card-img-caption .card-text { + position: absolute; + top: 0px; + right: 5px; + height: 5px; + cursor: pointer; +} + +.alert-delete { + margin-left: 1rem; + margin-right: 1rem; +} + +.card-user { + position: absolute; + top: 2px; + left: 5px; + height: 20px; + color: grey; + cursor: pointer; +} + +.tool { + background: white; + color: #888; + font-size: 18px; + border: 0; + padding-top: 0px; + vertical-align: bottom; +} + +.trash:hover { + background: white; + color: #d9534f; + font-size: 18px; + border: 0; + padding-top: 0px; + vertical-align: bottom; +} + +button.drop { + width: 90%; + margin-left:auto; + margin-right:auto; +} + +.remove { + margin-top: 1rem; +} + +.sortCards { + margin-top: 1%; + margin-right: 1%; + position:absolute; + top:0; + right:0; +} diff --git a/javascript/SectionForm/index.jsx b/javascript/Show/index.jsx similarity index 50% rename from javascript/SectionForm/index.jsx rename to javascript/Show/index.jsx index b4369e3..02953af 100644 --- a/javascript/SectionForm/index.jsx +++ b/javascript/Show/index.jsx @@ -1,7 +1,7 @@ 'use strict' import React from 'react' import ReactDOM from 'react-dom' -import Section from './Section.jsx' +import ShowView from './ShowView.jsx' ReactDOM.render( -
    , document.getElementById('sectionform')) + , document.getElementById('shows')) diff --git a/javascript/ShowForm/Form.jsx b/javascript/ShowForm/Form.jsx deleted file mode 100644 index 956e1c6..0000000 --- a/javascript/ShowForm/Form.jsx +++ /dev/null @@ -1,49 +0,0 @@ -'use strict' -import React from 'react' -import InputField from '../AddOn/Form/InputField.jsx' -import Show from '../Resources/Show.js' -import Abstract from '../AddOn/Mixin/Abstract.jsx' -import PropTypes from 'prop-types' - -/* global $ */ - -export default class ShowForm extends Abstract { - constructor(props) { - super(props) - this.state = { - resource: Show, - errors: {title: false} - } - this.save = this.save.bind(this) - } - - save() { - $.ajax({ - url: './slideshow/Show', - data : this.state.resource, - dataType: 'json', - type: 'post', - success: function () { - this.props.success() - }.bind(this), - error: function () {}.bind(this), - }) - } - - render() { - return ( -
    - - -
    - ) - } -} - -ShowForm.propTypes = { - success: PropTypes.func.isRequired -} diff --git a/javascript/ShowForm/Show.jsx b/javascript/ShowForm/Show.jsx deleted file mode 100644 index 45f6206..0000000 --- a/javascript/ShowForm/Show.jsx +++ /dev/null @@ -1,46 +0,0 @@ -'use strict' -import React, {Component} from 'react' -import Modal from '../AddOn/Html/Modal.jsx' -import Form from './Form.jsx' - -/* global $, openStatus, slide */ - -export default class Section extends Component { - constructor(props) { - super(props) - this.state = {showForm:false} - this.initializeShowLink() - this.showForm = this.showForm.bind(this) - this.hideForm = this.hideForm.bind(this) - } - - initializeShowLink() { - $('#add-show').click(function () { - this.showForm() - }.bind(this)) - } - - showSaved() { - this.hideForm() - } - - showForm() { - this.setState({showForm: true}) - openStatus = false - slide() - } - - hideForm() { - this.setState({showForm: false}) - } - - backToShow() { - window.location.href = './slideshow/Show/list' - } - - render() { - return ( -
    - ) - } -} diff --git a/javascript/Slide/Decision.jsx b/javascript/Slide/Decision.jsx deleted file mode 100644 index b4c9b05..0000000 --- a/javascript/Slide/Decision.jsx +++ /dev/null @@ -1,32 +0,0 @@ -"use strict" -import React, { Component } from "react" -import PropTypes from "prop-types" -import DecisionList from "./DecisionList" - -export default class Decision extends Component { - constructor(props) { - super(props) - } - - render() { - return ( -
    - -
    - -
    -
    - ) - } -} - -Decision.propTypes = { - listing: PropTypes.array, - add: PropTypes.func, - showForm: PropTypes.func, -} diff --git a/javascript/Slide/DecisionForm.js b/javascript/Slide/DecisionForm.js deleted file mode 100644 index 7504fc8..0000000 --- a/javascript/Slide/DecisionForm.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict' -import React from 'react' -import PropTypes from 'prop-types' - -const DecisionForm = ({decision, update, save, deleteDecision,}) => { - - const updateTitle = (e) => { - decision.title = e.target.value - update(decision) - } - - const updateMessage = (e) => { - decision.message = e.target.value - update(decision) - } - - const updateNext = () => { - decision.next = decision.next == '1' ? '0' : '1' - update(decision) - } - - return ( -
    - - - - -
    -``` - -#### External Markdown - -You can write your content as a separate file and have reveal.js load it at runtime. Note the separator arguments which determine how slides are delimited in the external file: the `data-separator` attribute defines a regular expression for horizontal slides (defaults to `^\r?\n---\r?\n$`, a newline-bounded horizontal rule) and `data-separator-vertical` defines vertical slides (disabled by default). The `data-separator-notes` attribute is a regular expression for specifying the beginning of the current slide's speaker notes (defaults to `note:`). The `data-charset` attribute is optional and specifies which charset to use when loading the external file. - -When used locally, this feature requires that reveal.js [runs from a local web server](#full-setup). The following example customises all available options: - -```html -
    -
    -``` - -#### Element Attributes - -Special syntax (in html comment) is available for adding attributes to Markdown elements. This is useful for fragments, amongst other things. - -```html -
    - -
    -``` - -#### Slide Attributes - -Special syntax (in html comment) is available for adding attributes to the slide `
    ` elements generated by your Markdown. - -```html -
    - -
    -``` - -#### Configuring *marked* - -We use [marked](https://github.com/chjj/marked) to parse Markdown. To customise marked's rendering, you can pass in options when [configuring Reveal](#configuration): - -```javascript -Reveal.initialize({ - // Options which are passed into marked - // See https://github.com/chjj/marked#options-1 - markdown: { - smartypants: true - } -}); -``` - -### Configuration - -At the end of your page you need to initialize reveal by running the following code. Note that all config values are optional and will default as specified below. - -```javascript -Reveal.initialize({ - - // Display controls in the bottom right corner - controls: true, - - // Display a presentation progress bar - progress: true, - - // Set default timing of 2 minutes per slide - defaultTiming: 120, - - // Display the page number of the current slide - slideNumber: false, - - // Push each slide change to the browser history - history: false, - - // Enable keyboard shortcuts for navigation - keyboard: true, - - // Enable the slide overview mode - overview: true, - - // Vertical centering of slides - center: true, - - // Enables touch navigation on devices with touch input - touch: true, - - // Loop the presentation - loop: false, - - // Change the presentation direction to be RTL - rtl: false, - - // Randomizes the order of slides each time the presentation loads - shuffle: false, - - // Turns fragments on and off globally - fragments: true, - - // Flags if the presentation is running in an embedded mode, - // i.e. contained within a limited portion of the screen - embedded: false, - - // Flags if we should show a help overlay when the questionmark - // key is pressed - help: true, - - // Flags if speaker notes should be visible to all viewers - showNotes: false, - - // Global override for autolaying embedded media (video/audio/iframe) - // - null: Media will only autoplay if data-autoplay is present - // - true: All media will autoplay, regardless of individual setting - // - false: No media will autoplay, regardless of individual setting - autoPlayMedia: null, - - // Number of milliseconds between automatically proceeding to the - // next slide, disabled when set to 0, this value can be overwritten - // by using a data-autoslide attribute on your slides - autoSlide: 0, - - // Stop auto-sliding after user input - autoSlideStoppable: true, - - // Use this method for navigation when auto-sliding - autoSlideMethod: Reveal.navigateNext, - - // Enable slide navigation via mouse wheel - mouseWheel: false, - - // Hides the address bar on mobile devices - hideAddressBar: true, - - // Opens links in an iframe preview overlay - previewLinks: false, - - // Transition style - transition: 'slide', // none/fade/slide/convex/concave/zoom - - // Transition speed - transitionSpeed: 'default', // default/fast/slow - - // Transition style for full page slide backgrounds - backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom - - // Number of slides away from the current that are visible - viewDistance: 3, - - // Parallax background image - parallaxBackgroundImage: '', // e.g. "'https://s3.amazonaws.com/hakim-static/reveal-js/reveal-parallax-1.jpg'" - - // Parallax background size - parallaxBackgroundSize: '', // CSS syntax, e.g. "2100px 900px" - - // Number of pixels to move the parallax background per slide - // - Calculated automatically unless specified - // - Set to 0 to disable movement along an axis - parallaxBackgroundHorizontal: null, - parallaxBackgroundVertical: null, - - // The display mode that will be used to show slides - display: 'block' - -}); -``` - - -The configuration can be updated after initialization using the ```configure``` method: - -```javascript -// Turn autoSlide off -Reveal.configure({ autoSlide: 0 }); - -// Start auto-sliding every 5s -Reveal.configure({ autoSlide: 5000 }); -``` - - -### Presentation Size - -All presentations have a normal size, that is the resolution at which they are authored. The framework will automatically scale presentations uniformly based on this size to ensure that everything fits on any given display or viewport. - -See below for a list of configuration options related to sizing, including default values: - -```javascript -Reveal.initialize({ - - ... - - // The "normal" size of the presentation, aspect ratio will be preserved - // when the presentation is scaled to fit different resolutions. Can be - // specified using percentage units. - width: 960, - height: 700, - - // Factor of the display size that should remain empty around the content - margin: 0.1, - - // Bounds for smallest/largest possible scale to apply to content - minScale: 0.2, - maxScale: 1.5 - -}); -``` - -If you wish to disable this behavior and do your own scaling (e.g. using media queries), try these settings: - -```javascript -Reveal.initialize({ - - ... - - width: "100%", - height: "100%", - margin: 0, - minScale: 1, - maxScale: 1 -}); -``` - -### Dependencies - -Reveal.js doesn't _rely_ on any third party scripts to work but a few optional libraries are included by default. These libraries are loaded as dependencies in the order they appear, for example: - -```javascript -Reveal.initialize({ - dependencies: [ - // Cross-browser shim that fully implements classList - https://github.com/eligrey/classList.js/ - { src: 'lib/js/classList.js', condition: function() { return !document.body.classList; } }, - - // Interpret Markdown in
    elements - { src: 'plugin/markdown/marked.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } }, - { src: 'plugin/markdown/markdown.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } }, - - // Syntax highlight for elements - { src: 'plugin/highlight/highlight.js', async: true, callback: function() { hljs.initHighlightingOnLoad(); } }, - - // Zoom in and out with Alt+click - { src: 'plugin/zoom-js/zoom.js', async: true }, - - // Speaker notes - { src: 'plugin/notes/notes.js', async: true }, - - // MathJax - { src: 'plugin/math/math.js', async: true } - ] -}); -``` - -You can add your own extensions using the same syntax. The following properties are available for each dependency object: -- **src**: Path to the script to load -- **async**: [optional] Flags if the script should load after reveal.js has started, defaults to false -- **callback**: [optional] Function to execute when the script has loaded -- **condition**: [optional] Function which must return true for the script to be loaded - -To load these dependencies, reveal.js requires [head.js](http://headjs.com/) *(a script loading library)* to be loaded before reveal.js. - -### Ready Event - -A 'ready' event is fired when reveal.js has loaded all non-async dependencies and is ready to start navigating. To check if reveal.js is already 'ready' you can call `Reveal.isReady()`. - -```javascript -Reveal.addEventListener( 'ready', function( event ) { - // event.currentSlide, event.indexh, event.indexv -} ); -``` - -Note that we also add a `.ready` class to the `.reveal` element so that you can hook into this with CSS. - -### Auto-sliding - -Presentations can be configured to progress through slides automatically, without any user input. To enable this you will need to tell the framework how many milliseconds it should wait between slides: - -```javascript -// Slide every five seconds -Reveal.configure({ - autoSlide: 5000 -}); -``` -When this is turned on a control element will appear that enables users to pause and resume auto-sliding. Alternatively, sliding can be paused or resumed by pressing »a« on the keyboard. Sliding is paused automatically as soon as the user starts navigating. You can disable these controls by specifying ```autoSlideStoppable: false``` in your reveal.js config. - -You can also override the slide duration for individual slides and fragments by using the ```data-autoslide``` attribute: - -```html -
    -

    After 2 seconds the first fragment will be shown.

    -

    After 10 seconds the next fragment will be shown.

    -

    Now, the fragment is displayed for 2 seconds before the next slide is shown.

    -
    -``` - -To override the method used for navigation when auto-sliding, you can specify the ```autoSlideMethod``` setting. To only navigate along the top layer and ignore vertical slides, set this to ```Reveal.navigateRight```. - -Whenever the auto-slide mode is resumed or paused the ```autoslideresumed``` and ```autoslidepaused``` events are fired. - - -### Keyboard Bindings - -If you're unhappy with any of the default keyboard bindings you can override them using the ```keyboard``` config option: - -```javascript -Reveal.configure({ - keyboard: { - 13: 'next', // go to the next slide when the ENTER key is pressed - 27: function() {}, // do something custom when ESC is pressed - 32: null // don't do anything when SPACE is pressed (i.e. disable a reveal.js default binding) - } -}); -``` - -### Touch Navigation - -You can swipe to navigate through a presentation on any touch-enabled device. Horizontal swipes change between horizontal slides, vertical swipes change between vertical slides. If you wish to disable this you can set the `touch` config option to false when initializing reveal.js. - -If there's some part of your content that needs to remain accessible to touch events you'll need to highlight this by adding a `data-prevent-swipe` attribute to the element. One common example where this is useful is elements that need to be scrolled. - - -### Lazy Loading - -When working on presentation with a lot of media or iframe content it's important to load lazily. Lazy loading means that reveal.js will only load content for the few slides nearest to the current slide. The number of slides that are preloaded is determined by the `viewDistance` configuration option. - -To enable lazy loading all you need to do is change your "src" attributes to "data-src" as shown below. This is supported for image, video, audio and iframe elements. Lazy loaded iframes will also unload when the containing slide is no longer visible. - -```html -
    - - - -
    -``` - - -### API - -The ``Reveal`` object exposes a JavaScript API for controlling navigation and reading state: - -```javascript -// Navigation -Reveal.slide( indexh, indexv, indexf ); -Reveal.left(); -Reveal.right(); -Reveal.up(); -Reveal.down(); -Reveal.prev(); -Reveal.next(); -Reveal.prevFragment(); -Reveal.nextFragment(); - -// Randomize the order of slides -Reveal.shuffle(); - -// Toggle presentation states, optionally pass true/false to force on/off -Reveal.toggleOverview(); -Reveal.togglePause(); -Reveal.toggleAutoSlide(); - -// Shows a help overlay with keyboard shortcuts, optionally pass true/false -// to force on/off -Reveal.toggleHelp(); - -// Change a config value at runtime -Reveal.configure({ controls: true }); - -// Returns the present configuration options -Reveal.getConfig(); - -// Fetch the current scale of the presentation -Reveal.getScale(); - -// Retrieves the previous and current slide elements -Reveal.getPreviousSlide(); -Reveal.getCurrentSlide(); - -Reveal.getIndices(); // { h: 0, v: 0 } } -Reveal.getPastSlideCount(); -Reveal.getProgress(); // (0 == first slide, 1 == last slide) -Reveal.getSlides(); // Array of all slides -Reveal.getTotalSlides(); // total number of slides - -// Returns the speaker notes for the current slide -Reveal.getSlideNotes(); - -// State checks -Reveal.isFirstSlide(); -Reveal.isLastSlide(); -Reveal.isOverview(); -Reveal.isPaused(); -Reveal.isAutoSliding(); -``` - -### Slide Changed Event - -A 'slidechanged' event is fired each time the slide is changed (regardless of state). The event object holds the index values of the current slide as well as a reference to the previous and current slide HTML nodes. - -Some libraries, like MathJax (see [#226](https://github.com/hakimel/reveal.js/issues/226#issuecomment-10261609)), get confused by the transforms and display states of slides. Often times, this can be fixed by calling their update or render function from this callback. - -```javascript -Reveal.addEventListener( 'slidechanged', function( event ) { - // event.previousSlide, event.currentSlide, event.indexh, event.indexv -} ); -``` - -### Presentation State - -The presentation's current state can be fetched by using the `getState` method. A state object contains all of the information required to put the presentation back as it was when `getState` was first called. Sort of like a snapshot. It's a simple object that can easily be stringified and persisted or sent over the wire. - -```javascript -Reveal.slide( 1 ); -// we're on slide 1 - -var state = Reveal.getState(); - -Reveal.slide( 3 ); -// we're on slide 3 - -Reveal.setState( state ); -// we're back on slide 1 -``` - -### Slide States - -If you set ``data-state="somestate"`` on a slide ``
    ``, "somestate" will be applied as a class on the document element when that slide is opened. This allows you to apply broad style changes to the page based on the active slide. - -Furthermore you can also listen to these changes in state via JavaScript: - -```javascript -Reveal.addEventListener( 'somestate', function() { - // TODO: Sprinkle magic -}, false ); -``` - -### Slide Backgrounds - -Slides are contained within a limited portion of the screen by default to allow them to fit any display and scale uniformly. You can apply full page backgrounds outside of the slide area by adding a ```data-background``` attribute to your ```
    ``` elements. Four different types of backgrounds are supported: color, image, video and iframe. - -#### Color Backgrounds -All CSS color formats are supported, like rgba() or hsl(). -```html -
    -

    Color

    -
    -``` - -#### Image Backgrounds -By default, background images are resized to cover the full page. Available options: - -| Attribute | Default | Description | -| :--------------------------- | :--------- | :---------- | -| data-background-image | | URL of the image to show. GIFs restart when the slide opens. | -| data-background-size | cover | See [background-size](https://developer.mozilla.org/docs/Web/CSS/background-size) on MDN. | -| data-background-position | center | See [background-position](https://developer.mozilla.org/docs/Web/CSS/background-position) on MDN. | -| data-background-repeat | no-repeat | See [background-repeat](https://developer.mozilla.org/docs/Web/CSS/background-repeat) on MDN. | -```html -
    -

    Image

    -
    -
    -

    This background image will be sized to 100px and repeated

    -
    -``` - -#### Video Backgrounds -Automatically plays a full size video behind the slide. - -| Attribute | Default | Description | -| :--------------------------- | :------ | :---------- | -| data-background-video | | A single video source, or a comma separated list of video sources. | -| data-background-video-loop | false | Flags if the video should play repeatedly. | -| data-background-video-muted | false | Flags if the audio should be muted. | -| data-background-size | cover | Use `cover` for full screen and some cropping or `contain` for letterboxing. | - -```html -
    -

    Video

    -
    -``` - -#### Iframe Backgrounds -Embeds a web page as a slide background that covers 100% of the reveal.js width and height. The iframe is in the background layer, behind your slides, and as such it's not possible to interact with it by default. To make your background interactive, you can add the `data-background-interactive` attribute. -```html -
    -

    Iframe

    -
    -``` - -#### Background Transitions -Backgrounds transition using a fade animation by default. This can be changed to a linear sliding transition by passing ```backgroundTransition: 'slide'``` to the ```Reveal.initialize()``` call. Alternatively you can set ```data-background-transition``` on any section with a background to override that specific transition. - - -### Parallax Background - -If you want to use a parallax scrolling background, set the first two config properties below when initializing reveal.js (the other two are optional). - -```javascript -Reveal.initialize({ - - // Parallax background image - parallaxBackgroundImage: '', // e.g. "https://s3.amazonaws.com/hakim-static/reveal-js/reveal-parallax-1.jpg" - - // Parallax background size - parallaxBackgroundSize: '', // CSS syntax, e.g. "2100px 900px" - currently only pixels are supported (don't use % or auto) - - // Number of pixels to move the parallax background per slide - // - Calculated automatically unless specified - // - Set to 0 to disable movement along an axis - parallaxBackgroundHorizontal: 200, - parallaxBackgroundVertical: 50 - -}); -``` - -Make sure that the background size is much bigger than screen size to allow for some scrolling. [View example](http://lab.hakim.se/reveal-js/?parallaxBackgroundImage=https%3A%2F%2Fs3.amazonaws.com%2Fhakim-static%2Freveal-js%2Freveal-parallax-1.jpg¶llaxBackgroundSize=2100px%20900px). - - - -### Slide Transitions -The global presentation transition is set using the ```transition``` config value. You can override the global transition for a specific slide by using the ```data-transition``` attribute: - -```html -
    -

    This slide will override the presentation transition and zoom!

    -
    - -
    -

    Choose from three transition speeds: default, fast or slow!

    -
    -``` - -You can also use different in and out transitions for the same slide: - -```html -
    - The train goes on … -
    -
    - and on … -
    -
    - and stops. -
    -
    - (Passengers entering and leaving) -
    -
    - And it starts again. -
    -``` - - -### Internal links - -It's easy to link between slides. The first example below targets the index of another slide whereas the second targets a slide with an ID attribute (```
    ```): - -```html -Link -Link -``` - -You can also add relative navigation links, similar to the built in reveal.js controls, by appending one of the following classes on any element. Note that each element is automatically given an ```enabled``` class when it's a valid navigation route based on the current slide. - -```html - - - - - - -``` - - -### Fragments -Fragments are used to highlight individual elements on a slide. Every element with the class ```fragment``` will be stepped through before moving on to the next slide. Here's an example: http://lab.hakim.se/reveal-js/#/fragments - -The default fragment style is to start out invisible and fade in. This style can be changed by appending a different class to the fragment: - -```html -
    -

    grow

    -

    shrink

    -

    fade-out

    -

    fade-up (also down, left and right!)

    -

    visible only once

    -

    blue only once

    -

    highlight-red

    -

    highlight-green

    -

    highlight-blue

    -
    -``` - -Multiple fragments can be applied to the same element sequentially by wrapping it, this will fade in the text on the first step and fade it back out on the second. - -```html -
    - - I'll fade in, then out - -
    -``` - -The display order of fragments can be controlled using the ```data-fragment-index``` attribute. - -```html -
    -

    Appears last

    -

    Appears first

    -

    Appears second

    -
    -``` - -### Fragment events - -When a slide fragment is either shown or hidden reveal.js will dispatch an event. - -Some libraries, like MathJax (see #505), get confused by the initially hidden fragment elements. Often times this can be fixed by calling their update or render function from this callback. - -```javascript -Reveal.addEventListener( 'fragmentshown', function( event ) { - // event.fragment = the fragment DOM element -} ); -Reveal.addEventListener( 'fragmenthidden', function( event ) { - // event.fragment = the fragment DOM element -} ); -``` - -### Code syntax highlighting - -By default, Reveal is configured with [highlight.js](https://highlightjs.org/) for code syntax highlighting. Below is an example with clojure code that will be syntax highlighted. When the `data-trim` attribute is present, surrounding whitespace is automatically removed. HTML will be escaped by default. To avoid this, for example if you are using `` to call out a line of code, add the `data-noescape` attribute to the `` element. - -```html -
    -
    
    -(def lazy-fib
    -  (concat
    -   [0 1]
    -   ((fn rfib [a b]
    -        (lazy-cons (+ a b) (rfib b (+ a b)))) 0 1)))
    -	
    -
    -``` - -### Slide number -If you would like to display the page number of the current slide you can do so using the ```slideNumber``` and ```showSlideNumber``` configuration values. - -```javascript -// Shows the slide number using default formatting -Reveal.configure({ slideNumber: true }); - -// Slide number formatting can be configured using these variables: -// "h.v": horizontal . vertical slide number (default) -// "h/v": horizontal / vertical slide number -// "c": flattened slide number -// "c/t": flattened slide number / total slides -Reveal.configure({ slideNumber: 'c/t' }); - -// Control which views the slide number displays on using the "showSlideNumber" value: -// "all": show on all views (default) -// "speaker": only show slide numbers on speaker notes view -// "print": only show slide numbers when printing to PDF -Reveal.configure({ showSlideNumber: 'speaker' }); - -``` - - -### Overview mode - -Press "Esc" or "o" keys to toggle the overview mode on and off. While you're in this mode, you can still navigate between slides, -as if you were at 1,000 feet above your presentation. The overview mode comes with a few API hooks: - -```javascript -Reveal.addEventListener( 'overviewshown', function( event ) { /* ... */ } ); -Reveal.addEventListener( 'overviewhidden', function( event ) { /* ... */ } ); - -// Toggle the overview mode programmatically -Reveal.toggleOverview(); -``` - - -### Fullscreen mode -Just press »F« on your keyboard to show your presentation in fullscreen mode. Press the »ESC« key to exit fullscreen mode. - - -### Embedded media -Add `data-autoplay` to your media element if you want it to automatically start playing when the slide is shown: - -```html - -``` - -If you want to enable or disable autoplay globally, for all embedded media, you can use the `autoPlayMedia` configuration option. If you set this to `true` ALL media will autoplay regardless of individual `data-autoplay` attributes. If you initialize with `autoPlayMedia: false` NO media will autoplay. - -Note that embedded HTML5 `