diff options
-rwxr-xr-x | .htaccess | 17 | ||||
-rw-r--r-- | LICENSE | 4 | ||||
-rw-r--r-- | README.markdown | 38 | ||||
-rw-r--r-- | autoload/Todo/Lists.php | 141 | ||||
-rw-r--r-- | autoload/Todo/Model/TodoList.php | 100 | ||||
-rw-r--r-- | autoload/Todo/Model/User.php | 66 | ||||
-rw-r--r-- | autoload/Todo/SessionHelper.php | 49 | ||||
-rw-r--r-- | autoload/Todo/UserManager.php | 203 | ||||
-rwxr-xr-x | config.ini | 7 | ||||
-rwxr-xr-x | index.php | 41 | ||||
-rw-r--r-- | ui/blank.html | 0 | ||||
-rw-r--r-- | ui/deleteAccount.html | 6 | ||||
-rw-r--r-- | ui/deleteList.html | 8 | ||||
-rw-r--r-- | ui/img/sprites.png | bin | 0 -> 924 bytes | |||
-rw-r--r-- | ui/img/sprites.xcf | bin | 0 -> 13642 bytes | |||
-rw-r--r-- | ui/list.html | 46 | ||||
-rw-r--r-- | ui/login.html | 8 | ||||
-rw-r--r-- | ui/mails/activationcode | 6 | ||||
-rw-r--r-- | ui/mails/pwreset | 11 | ||||
-rw-r--r-- | ui/master.html | 63 | ||||
-rw-r--r-- | ui/noListSelected.html | 1 | ||||
-rw-r--r-- | ui/pwreset.html | 7 | ||||
-rw-r--r-- | ui/pwresetRequest.html | 6 | ||||
-rw-r--r-- | ui/register.html | 10 | ||||
-rw-r--r-- | ui/settings.html | 8 | ||||
-rw-r--r-- | ui/style.css | 306 | ||||
-rw-r--r-- | ui/welcome.html | 3 |
27 files changed, 1155 insertions, 0 deletions
diff --git a/.htaccess b/.htaccess new file mode 100755 index 0000000..68d1f73 --- /dev/null +++ b/.htaccess @@ -0,0 +1,17 @@ +# Enable rewrite engine and route requests to framework +RewriteEngine On + +# Some servers require you to specify the `RewriteBase` directive +# In such cases, it should be the path (relative to the document root) +# containing this .htaccess file +# +# RewriteBase / + +RewriteCond %{REQUEST_URI} \.ini$ +RewriteRule \.ini$ - [R=404] + +RewriteCond %{REQUEST_FILENAME} !-l +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule .* index.php [L,QSA] +RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L] @@ -0,0 +1,4 @@ + DO WHATEVER THE FUCK YOU WANT, PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHATEVER THE FUCK YOU WANT. diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..14fe1cc --- /dev/null +++ b/README.markdown @@ -0,0 +1,38 @@ +# todo + +A simple todo web application. + +## Install + +First, you need to fetch some dependencies: + +### Fat-Free Framework + +Download the Fat-Free Framework from [http://fatfreeframework.com/](http://fatfreeframework.com/) and extract the content of the `lib` folder into the `lib` folder of this application. + +### DejaVu Sans ExtraLight + +Get this light front from [http://www.fonts2u.com/download/dejavu-sans-extralight.font-face](http://www.fonts2u.com/download/dejavu-sans-extralight.font-face) and extract the files into the `ui/DejaVu_Sans_ExtraLight` directory + +Now that all dependencies are fetched, you can copy the application to your webserver. Just one thing: Change the permissions of the `tmp` directory so the webserver can write to it. + +## Configuration + +Todo is configured with the `config.ini` file. + +* `DEBUG` – Unless you intend to further develop this application, leave this to `0` +* `sql_dsn` – The [PDO DSN]() for connecting to the database +* `sql_user` – Username for the database access +* `sql_pass` – Password for the database access +* `mail_from` – Outgoing mails (confirmation codes, password reset mails) will use this E-Mail address for the `From` field +* `appname` – The name of the application + +### Protecting the configuration file + +It is usually a bad idea that the config file is accessible via the web, since people can then see the username and password of your database. + +One possibility to prevent this, is to move the file into a directory that can not be accessed via the web and then modifying `index.php`: Search for the line `$f3->config('config.ini');` and change the `config.ini` part to point to the new location. + +## Or just use my public installation + +[todo.kch42.net](http://todo.kch42.net/) diff --git a/autoload/Todo/Lists.php b/autoload/Todo/Lists.php new file mode 100644 index 0000000..9f951a3 --- /dev/null +++ b/autoload/Todo/Lists.php @@ -0,0 +1,141 @@ +<?php + +namespace Todo; + +class Lists extends SessionHelper { + public function home($f3) { + $f3->set('content', ($this->user === NULL) ? 'login.html' : 'noListSelected.html'); + } + + private function populateListData($f3, $l) { + $f3->set('listid', $l->id); + $f3->set('listname', $l->name); + $f3->set('listdata', $l->itemsToArray()); + } + + private function getList($f3, $id) { + $l = new \Todo\Model\TodoList($f3->get('DB')); + if((!$l->byID($id)) || $l->user !== $this->user->id) { + $f3->set('content', 'blank.html'); + $f3->set('error', 'List not found'); + $f3->error(404); # TODO: Meh, this stops F3 from rendering the template.... + return NULL; + } + return $l; + } + + public function deleteList($f3, $args) { + if($this->needLogin($f3)) { + return; + } + + if(!($l = $this->getList($f3, $args['list']))) { + return; + } + + if(($f3->get('VERB') === 'POST') && ($f3->get('POST.confirm') === 'OK')) { + $l->deleteList(); + + $f3->set('success', 'List deleted'); + $f3->set('content', 'blank.html'); + } else { + $this->populateListData($f3, $l); + $f3->set('content', 'deleteList.html'); + } + } + + public function newList($f3) { + if($this->needLogin($f3)) { + return; + } + + if($f3->get('VERB') !== 'POST') { + return; + } + + if($f3->get('POST.listname') === '') { + $f3->set('error', 'List name must not be empty'); + $f3->set('content', 'blank.html'); + return; + } + + $l = new \Todo\Model\TodoList($f3->get('DB')); + $l->user = $this->user->id; + $l->name = $f3->get('POST.listname'); + $l->save(); + + $f3->set('success', 'List created!'); + + $f3->set('content', 'list.html'); + $this->populateListData($f3, $l); + } + + public function showList($f3, $args) { + if($this->needLogin($f3)) { + return; + } + + $f3->set('content', 'list.html'); + + if(!($l = $this->getList($f3, $args['list']))) { + return; + } + + if($f3->get('VERB') === 'POST') { + $ok = true; + switch($f3->get('POST.action')) { + case 'setname': + if($f3->get('POST.name') === '') { + $f3->set('error', 'List name must not be empty'); + $ok = false; + } else { + $l->name = $f3->get('POST.name'); + $l->save(); + } + break; + case 'additem': + if($f3->get('POST.itemtext') === '') { + $f3->set('error', 'Can not add empty list item'); + $ok = false; + } else { + $l->addItem($f3->get('POST.itemtext')); + } + break; + case 'delitem': + $l->delItem($f3->get('POST.id')); + break; + case 'setchecked': + $it = $l->getItem($f3->get('POST.id')); + if($it === NULL) { + $f3->set('error', 'Invalid item ID'); + $ok = false; + } else { + $it->checked = ($f3->get('POST.checked') === 'y'); + $it->save(); + } + break; + case 'moveup': + $l->moveItem($f3->get('POST.id'), -1); + break; + case 'movedown': + $l->moveItem($f3->get('POST.id'), 1); + break; + case 'movex': + $l->moveItem($f3->get('POST.id'), $f3->get('POST.move')); + break; + case 'delchecked': + $l->deleteChecked(); + break; + default: + $ok = false; + $f3->set('error', 'Unknown list action.'); + } + + if($ok) { + $f3->set('success', 'List updated'); + } + } + + $this->populateListData($f3, $l); + } +}
\ No newline at end of file diff --git a/autoload/Todo/Model/TodoList.php b/autoload/Todo/Model/TodoList.php new file mode 100644 index 0000000..4470c34 --- /dev/null +++ b/autoload/Todo/Model/TodoList.php @@ -0,0 +1,100 @@ +<?php + +namespace Todo\Model; + +class TodoList extends \DB\SQL\Mapper { + public function __construct($db) { + parent::__construct($db, 'lists'); + } + + public function byID($id) { + $this->load(array('id=?', $id)); + return !$this->dry(); + } + + public function itemsToArray() { + $items = array(); + $it = new \DB\SQL\Mapper($this->db, 'items'); + for($it->load(array('list=?', $this->id), array('order' => 'ord ASC')); !$it->dry(); $it->next()) { + $items[] = array( + 'id' => $it->id, + 'ord' => $it->ord, + 'text' => $it->text, + 'date' => $it->date, + 'checked' => $it->checked, + ); + } + + return $items; + } + + public function countItems() { + $it = new \DB\SQL\Mapper($this->db, 'items'); + return $it->count(array('list=?', $this->id)); + } + + public function getItem($id) { + $it = new \DB\SQL\Mapper($this->db, 'items'); + $it->load(array('list=? AND id=?', $this->id, $id)); + return $it->dry() ? NULL : $it; + } + + public function moveItem($id, $movement) { + $it = new \DB\SQL\Mapper($this->db, 'items'); + $it->load(array('list=? AND id=?', $this->id, $id)); + if($it->dry()) { + return; + } + $ordOld = $it->ord; + $it->reset(); + + $ordNew = min(max(0, $ordOld + $movement), $this->countItems() - 1); + if($ordNew === $ordOld) { + return; + } + + $moveSgn = ($movement > 0) ? 1 : -1; + + if($moveSgn === 1) { + $it->load(array('list=? AND ord>? AND ord<=?', $this->id, $ordOld, $ordNew)); + } else { + $it->load(array('list=? AND ord>=? AND ord<?', $this->id, $ordNew, $ordOld)); + } + for(;!$it->dry(); $it->next()) { + $it->ord -= $moveSgn; + $it->save(); + } + + $it->reset(); + $it->load(array('list=? AND id=?', $this->id, $id)); + $it->ord = $ordNew; + $it->save(); + } + + public function addItem($text) { + $n = $this->countItems(); + + $now = new \DateTime('now'); + + $it = new \DB\SQL\Mapper($this->db, 'items'); + $it->text = $text; + $it->ord = $n; + $it->date = $now->format('Y-m-d H:i:s'); + $it->checked = false; + $it->list = $this->id; + $it->save(); + } + + public function delItem($id) { + $this->db->exec('DELETE FROM `items` WHERE `list` = :l AND `id` = :i', array(':l' => $this->id, ':i' => $id)); + } + + public function deleteList() { + $this->db->exec('DELETE FROM `items` WHERE `list` = :l', array(':l' => $this->id)); + $this->erase(); + } + + public function deleteChecked() { + $this->db->exec('DELETE FROM `items` WHERE `list` = :l AND `checked` = :c', array(':l' => $this->id, ':c' => true)); + } +}
\ No newline at end of file diff --git a/autoload/Todo/Model/User.php b/autoload/Todo/Model/User.php new file mode 100644 index 0000000..53c8f67 --- /dev/null +++ b/autoload/Todo/Model/User.php @@ -0,0 +1,66 @@ +<?php + +namespace Todo\Model; + +class User extends \DB\SQL\Mapper { + public function __construct(\DB\SQL $db) { + parent::__construct($db, 'users'); + } + + public function makeCode() { + $alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + $len = strlen($alphabet); + $this->code = ''; + for($i = 0; $i < 16; $i++) { + $this->code .= substr($alphabet, mt_rand(0, $len-1), 1); + } + } + + public function register($name, $email, $password) { + $this->load(array('name=? OR email=?', $name, $email)); + if(!$this->dry()) { + $this->reset(); + return false; + } + + $this->name = $name; + $this->email = $email; + $this->pwhash = \Bcrypt::instance()->hash($password); + $this->active = false; + $this->makeCode(); + $this->save(); + return true; + } + + public function byName($name) { + $this->load(array('name=?', $name)); + return !$this->dry(); + } + + public function byID($id) { + $this->load(array('id=?', $id)); + return !$this->dry(); + } + + public function byEmail($email) { + $this->load(array('email=?', $email)); + return !$this->dry(); + } + + public function verifyPass($password) { + return \Bcrypt::instance()->verify($password, $this->pwhash); + } + + public function deleteUser() { + $this->db->exec('DELETE FROM `items` WHERE `list` IN (SELECT `id` FROM `lists` WHERE `user` = :u)', array(':u' => $this->id)); + $this->db->exec('DELETE FROM `lists` WHERE `user` = :u', array(':u' => $this->id)); + $this->erase(); + } + + # Returns a TodoList object that can be iterated with ->next(). + public function lists() { + $l = new \Todo\Model\TodoList($this->db); + $l->load(array('user=?', $this->id), array('order' => 'name ASC')); + return $l; + } +}
\ No newline at end of file diff --git a/autoload/Todo/SessionHelper.php b/autoload/Todo/SessionHelper.php new file mode 100644 index 0000000..b4811ab --- /dev/null +++ b/autoload/Todo/SessionHelper.php @@ -0,0 +1,49 @@ +<?php + +namespace Todo; + +abstract class SessionHelper { + protected $user = NULL; + + private function populateLists($f3) { + $lists = array(); + for($l = $this->user->lists(); !$l->dry(); $l->next()) { + $lists[] = array( + 'id' => $l->id, + 'name' => $l->name, + 'active' => ($l->id === $f3->get('listid')), + ); + } + $f3->set('lists', $lists); + } + + public function beforeRoute($f3) { + $id = $f3->get('SESSION.userID'); + if(is_numeric($id) && ($id > 0)) { + $this->user = new \Todo\Model\User($f3->get('DB')); + if((!$this->user->byID($id)) || (!$this->user->active)) { + $this->user = NULL; + } + } + $f3->set('listid', -1); + } + + public function afterRoute($f3) { + $f3->set('SESSION.userID', ($this->user === NULL) ? -1 : $this->user->id); + if($this->user !== NULL) { + $f3->set('user', $this->user->name); + $this->populateLists($f3); + } else { + $f3->set('user', ''); + } + } + + protected function needLogin($f3) { + if($this->user === NULL) { + $f3->set('error', 'You must be logged in to do that'); + $f3->set('content', 'blank.html'); + return true; + } + return false; + } +}
\ No newline at end of file diff --git a/autoload/Todo/UserManager.php b/autoload/Todo/UserManager.php new file mode 100644 index 0000000..306eb02 --- /dev/null +++ b/autoload/Todo/UserManager.php @@ -0,0 +1,203 @@ +<?php + +namespace Todo; + +class UserManager extends \Todo\SessionHelper { + public function login($f3, $args) { + if($this->user !== NULL) { + $f3->set('info', 'You are already logged in'); + $f3->set('content', 'blank.html'); + return; + } + $f3->set('content', 'login.html'); + + if($f3->get('VERB') !== "POST") { + return; + } + + $this->user = new \Todo\Model\User($f3->get('DB')); + if($this->user->byName($f3->get('POST.name')) && $this->user->verifyPass($f3->get('POST.password')) && $this->user->active) { + $f3->set('success', 'Login Successful!'); + $f3->set('content', 'noListSelected.html'); + } else { + $this->user = NULL; + $f3->set('error', 'Username or password is wrong or account is not active.'); + } + } + + public function logout($f3, $args) { + $this->user = NULL; + $f3->set('success', 'Logout Successful!'); + $f3->set('content', 'login.html'); + } + + public function register($f3, $args) { + if($this->user !== NULL) { + $f3->set('error', 'You already have an account!'); + $f3->set('content', 'blank.html'); + return; + } + $f3->set('content', 'register.html'); + + if($f3->get('VERB') !== 'POST') { + return; + } + + if(($f3->get('POST.password') === '') || ($f3->get('POST.name') === '') || ($f3->get('POST.email') === '')) { + $f3->set('error', 'All form inputs must be filled out'); + return; + } + + if($f3->get('POST.password') !== $f3->get('POST.repeatPassword')) { + $f3->set('error', 'Passwords do not match'); + return; + } + + $u = new \Todo\Model\User($f3->get('DB')); + if($u->register($f3->get('POST.name'), $f3->get('POST.email'), $f3->get('POST.password'))) { + $f3->set('username', $u->name); + $f3->set('activationurl', 'http://'.$f3->get('HOST').$f3->get('BASE').'/activate/'.$u->id.'/'.$u->code); + mail($u->email, 'Activation code for ' . $f3->get('appname'), \Template::instance()->render('mails/activationcode', 'text/plain'), 'From:'.$f3->get('mail_from')); + + $f3->set('success', 'New account created. Check your mails for the activation link.'); + $f3->set('content', 'blank.html'); + } else { + $f3->set('error', 'Name or E-Mail already in use.'); + } + } + + public function delete($f3, $args) { + if($this->needLogin($f3)) { + return; + } + + $f3->set('content', 'deleteAccount.html'); + + if(($f3->get('VERB') == 'POST') && ($f3->get('POST.confirm') == 'OK')) { + $this->user->deleteUser(); + $this->user = NULL; + $f3->set('success', 'Account deleted!'); + $f3->set('content', 'blank.html'); + } + } + + public function activate($f3, $args) { + $f3->set('content', 'blank.html'); + + $u = new \Todo\Model\User($f3->get('DB')); + if(!$u->byID($args['user'])) { + $f3->set('error', 'Unknown user.'); + return; + } + + if($u->active) { + $f3->set('info', 'Account already activated'); + return; + } + + if($u->code !== $args['code']) { + $f3->set('error', 'Wrong activation code!'); + return; + } + + $u->active = true; + $u->makeCode(); # set a new random code to prevent double usage + $u->save(); + + $f3->set('success', 'Account activated!'); + } + + public function initResetpw($f3, $args) { + $f3->set('content', 'pwresetRequest.html'); + + if($f3->get('VERB') !== 'POST') { + return; + } + + $u = new \Todo\Model\User($f3->get('DB')); + if(!$u->byEmail($f3->get('POST.email'))) { + $f3->set('error', 'No account with this address registered.'); + return; + } + + $u->makeCode(); + $u->save(); + + $f3->set('username', $u->name); + $f3->set('reseturl', 'http://'.$f3->get('HOST').$f3->get('BASE').'/pwreset/'.$u->id.'/'.$u->code); + mail($u->email, 'Password reset for ' . $f3->get('appname'), \Template::instance()->render('mails/pwreset', 'text/plain'), 'From:'.$f3->get('mail_from')); + + $f3->set('success', 'Password reset link was sent to your E-Mail address.'); + $f3->set('content', 'blank.html'); + } + + public function resetpw($f3, $args) { + $u = new \Todo\Model\User($f3->get('DB')); + if((!$u->byID($args['user'])) || ($u->code !== $args['code'])) { + $f3->set('error', 'Invalid password reset link.'); + $f3->set('content', 'blank.html'); + return; + } + + $f3->set('content', 'pwreset.html'); + if($f3->get('VERB') !== 'POST') { + return; + } + + if($f3->get('POST.password') !== $f3->get('POST.repeatPassword')) { + $f3->set('error', 'Passwords do not match'); + return; + } + + $u->pwhash = \Bcrypt::instance()->hash($f3->get('POST.password')); + $u->makeCode(); + $u->save(); + + $f3->set('success', 'Password changed'); + $f3->set('content', 'blank.html'); + } + + public function settings($f3, $args) { + if($this->needLogin($f3)) { + return; + } + + $f3->set('content', 'settings.html'); + $f3->set('email', $this->user->email); + + if($f3->get('VERB') !== 'POST') { + return; + } + + $ok = array(); + $error = array(); + + if(($this->user->email !== $f3->get('POST.email')) && ($f3->get('POST.email') !== '')) { + $this->user->email = $f3->get('POST.email'); + $f3->set('email', $this->user->email); + $ok[] = 'E-Mail address changed.'; + } + + if($f3->get('POST.password') !== '') { + if($f3->get('POST.password') === $f3->get('POST.repeatPassword')) { + $this->user->pwhash = \Bcrypt::instance()->hash($f3->get('POST.password')); + $ok[] = 'Password changed.'; + } else { + $error[] = 'Passwords do not match.'; + } + } + + if(!empty($ok)) { + $this->user->save(); + } + + $text = array_reduce(array_merge($ok, $error), function($a, $b) { return $a . ' ' . $b; }); + if((!empty($ok)) && (!empty($error))) { + $f3->set('info', $text); + } else if(!empty($ok)) { + $f3->set('success', $text); + } else if(!empty($error)) { + $f3->set('error', $text); + } + } +} diff --git a/config.ini b/config.ini new file mode 100755 index 0000000..b1f4e23 --- /dev/null +++ b/config.ini @@ -0,0 +1,7 @@ +[globals]
+DEBUG=0
+sql_dsn="mysql:host=localhost;dbname=DATABASE"
+sql_user=USER
+sql_pass=PASSWORD
+mail_from=no-reply@your-domain
+appname=Todo
diff --git a/index.php b/index.php new file mode 100755 index 0000000..fc9e213 --- /dev/null +++ b/index.php @@ -0,0 +1,41 @@ +<?php
+
+$f3 = require('lib/base.php');
+
+$f3->set('AUTOLOAD','autoload/');
+$f3->set('UI', 'ui/');
+
+$f3->config('config.ini');
+
+# Init DB
+$db = new DB\SQL($f3->get('sql_dsn'), $f3->get('sql_user'), $f3->get('sql_pass'));
+$f3->set("DB", $db);
+
+# Init Sessions
+new Db\SQL\Session($db);
+
+# Homepage
+$f3->route('GET @root: /', 'Todo\Lists->home');
+
+# User management stuff
+$f3->route('GET|POST @login: /login', 'Todo\UserManager->login');
+$f3->route('GET @logout: /logout', 'Todo\UserManager->logout');
+$f3->route('GET|POST @register: /register', 'Todo\UserManager->register');
+$f3->route('GET|POST @delete_acc: /delete_acc', 'Todo\UserManager->delete');
+$f3->route('GET /activate/@user/@code', 'Todo\UserManager->activate');
+$f3->route('GET|POST @resetpw: /resetpw', 'Todo\UserManager->initResetpw');
+$f3->route('GET|POST /resetpw/@user/@code', 'Todo\UserManager->resetpw');
+$f3->route('GET|POST @settings: /settings', 'Todo\UserManager->settings');
+
+# List stuff
+$f3->route('POST @newlist: /newlist', 'Todo\Lists->newList');
+$f3->route('GET|POST /list/@list', 'Todo\Lists->showList');
+$f3->route('GET|POST /list/@list/delete', 'Todo\Lists->deleteList');
+
+$f3->route('GET /foo', function($f3) {
+ $f3->set('content', 'blank.html');
+ $f3->set('info', 'http://'.$f3->get('HOST').$f3->get('BASE'));
+});
+
+$f3->run();
+echo Template::instance()->render('master.html');
diff --git a/ui/blank.html b/ui/blank.html new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ui/blank.html diff --git a/ui/deleteAccount.html b/ui/deleteAccount.html new file mode 100644 index 0000000..565082f --- /dev/null +++ b/ui/deleteAccount.html @@ -0,0 +1,6 @@ +<form action="{{ @BASE . @ALIASES.delete_acc }}" method="post"> + <input type="hidden" name="confirm" value="OK" /> + <p>Do you really want to delete your account?</p> + <p><a href="{{ @BASE . @ALIASES.root }}">No</a></p> + <p><input type="submit" value="Yes" /></p> +</form>
\ No newline at end of file diff --git a/ui/deleteList.html b/ui/deleteList.html new file mode 100644 index 0000000..a71af7f --- /dev/null +++ b/ui/deleteList.html @@ -0,0 +1,8 @@ +<form action="{{ @BASE }}/list/{{ @listid }}/delete" method="post"> + <input type="hidden" name="confirm" value="OK" /> + <p>Do you really want to delete the list "{{ @listname }}"?</p> + <p> + <a class="fakebutton" href="{{ @BASE . @ALIASES.root }}">No</a> + <button type="submit">Yes</button> + </p> +</form>
\ No newline at end of file diff --git a/ui/img/sprites.png b/ui/img/sprites.png Binary files differnew file mode 100644 index 0000000..b3af1e4 --- /dev/null +++ b/ui/img/sprites.png diff --git a/ui/img/sprites.xcf b/ui/img/sprites.xcf Binary files differnew file mode 100644 index 0000000..830a6a5 --- /dev/null +++ b/ui/img/sprites.xcf diff --git a/ui/list.html b/ui/list.html new file mode 100644 index 0000000..ce998dd --- /dev/null +++ b/ui/list.html @@ -0,0 +1,46 @@ +<set myurl="{{ @BASE . '/list/' . @listid }}" /> +<form action="{{ @myurl }}" method="post"> + <h1> + <input type="hidden" name="action" value="setname" /> + <input type="text" name="name" value="{{ @listname }}" class="listname" /> + <button type="submit" title="Set name">✓</button> + </h1> +</form> + +<check if="{{ empty(@listdata) }}"> + <true><div class="empty">List is empty</div></true> + <false> + <ul class="todolist"> + <repeat group="{{ @listdata }}" value="{{ @it }}"> + <li{{ @it.checked ? ' class="checked"' : '' }}> + <form action="{{ @myurl }}" method="post"> + <input type="hidden" name="checked" value="{{ @it.checked ? 'n' : 'y' }}" /> + <button type="submit" name="action" value="setchecked" class="chkbtn {{ @it.checked ? 'checked' : '' }}"><input type="checkbox" name="checked_now" disabled="disabled" {{ @it.checked ? 'checked="checked"' : '' }} value="y" /></button> + + <span class="txt">{{ @it.text }}</span> + + <input type="hidden" name="id" value="{{ @it.id }}" /> + <span class="btncluster"> + <button type="submit" name="action" value="moveup" title="Move up">↑</button> + <button type="submit" name="action" value="movedown" title="Move down">↓</button> + <button type="submit" name="action" value="delitem" title="Delete">X</button> + </span> + </form> + </li> + </repeat> + </ul> + <div class="delchecked"> + <form action="{{ @myurl }}" method="post"> + <button type="submit" name="action" value="delchecked">Delete checked items</button> + </form> + </div> + </false> +</check> + +<div class="additem"> + <form action="{{ @myurl }}/" method="post"> + <input type="hidden" name="action" value="additem" /> + <input type="text" name="itemtext" /> + <button type="submit" title="Add item">+</button> + </form> +</div>
\ No newline at end of file diff --git a/ui/login.html b/ui/login.html new file mode 100644 index 0000000..0b8d2e4 --- /dev/null +++ b/ui/login.html @@ -0,0 +1,8 @@ +<h1>Login</h1> + +<form action="{{ @BASE . @ALIASES.login }}" method="post" class="tabform"> + <p><label for="login_name">User:</label> <input id="login_name" type="text" name="name" value="" /></p> + <p><label for="login_pass">Password:</label> <input id="login_pass" type="password" name="password" /></p> + <p class="formctrl"><input type="submit" /></p> +</form> +<p><a href="{{ @BASE . @ALIASES.resetpw }}">Forgot Password?</a> | <a href="{{ @BASE . @ALIASES.register }}">Register a new account</a></p>
\ No newline at end of file diff --git a/ui/mails/activationcode b/ui/mails/activationcode new file mode 100644 index 0000000..bb46bfd --- /dev/null +++ b/ui/mails/activationcode @@ -0,0 +1,6 @@ +Hi, {{ @username }}! + +You have registered an account for {{ @appname }}. +To activate the account, visit this URL: + +{{ @activationurl }}
\ No newline at end of file diff --git a/ui/mails/pwreset b/ui/mails/pwreset new file mode 100644 index 0000000..2c69c37 --- /dev/null +++ b/ui/mails/pwreset @@ -0,0 +1,11 @@ +Hi, {{ @username }}! + +You have requested a password reset four your account on +{{ @appname }}. + +To reset you password, use this URL: + +{{ @reseturl }} + +If you did NOT request a password reset, you can simply ignore this +email. diff --git a/ui/master.html b/ui/master.html new file mode 100644 index 0000000..25775f9 --- /dev/null +++ b/ui/master.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<html> +<head> + <title>Todo</title> + <link rel="stylesheet" type="text/css" href="{{ @BASE }}/ui/style.css" /> +</head> +<body> + <div id="headerwrap"><div id="header"> + <a href="{{ @BASE . @ALIASES.root }}" id="appname">{{ @appname }}</a> + <span id="userinfo"> + <check if="{{ empty(@user) }}"> + <true> + <a href="{{ @BASE . @ALIASES.login }}">Login</a> + </true> + <false> + Logged in as <span class="username">{{ @user }}</span>. + <a href="{{ @BASE . @ALIASES.settings }}">Settings</a> + <a href="{{ @BASE . @ALIASES.logout }}">Logout</a> + </false> + </check> + </span> + </div></div> + + <div class="bghelp"></div> + <div id="lists"> + <check if="{{ !empty(@user) }}"> + <ul> + <check if="{{ !empty(@lists) }}"> + <true> + <repeat group="{{ @lists }}" value="{{ @l }}"> + <li{{ @l.active ? ' class="active"' : '' }}> + <a href="{{ @BASE . '/list/' . @l.id }}" class="listlink">{{ @l.name }}</a> + <a class="deletelist" href="{{ @BASE . '/list/' . @l.id }}/delete" title="delete">X</a> + </li> + </repeat> + </true> + <false> + <li class="empty">No lists here. Why not add one below?</li> + </false> + </check> + </ul> + + <form id="addlist" action="{{ @BASE . @ALIASES.newlist }}" method="post"> + <input type="text" name="listname" /><button type="submit">+</button> + </form> + </check> + </div> + + <div id="content"> + <check if="{{ !empty(@success) }}"> + <div class="success notify"><span class="icon"></span>{{ @success }}</div> + </check> + <check if="{{ !empty(@error) }}"> + <div class="error notify"><span class="icon"></span>{{ @error }}</div> + </check> + <check if="{{ !empty(@info) }}"> + <div class="info notify"><span class="icon"></span>{{ @info }}</div> + </check> + + <include href="{{ @content }}" /> + </div> +</body> +</html>
\ No newline at end of file diff --git a/ui/noListSelected.html b/ui/noListSelected.html new file mode 100644 index 0000000..053b8e3 --- /dev/null +++ b/ui/noListSelected.html @@ -0,0 +1 @@ +<div class="nolist">No list selected</div>
\ No newline at end of file diff --git a/ui/pwreset.html b/ui/pwreset.html new file mode 100644 index 0000000..8d563d0 --- /dev/null +++ b/ui/pwreset.html @@ -0,0 +1,7 @@ +<h1>Password reset</h1> + +<form action="{{ @URI }}" method="post"> + <p>New Password: <input type="password" name="password" /></p> + <p>Retype Password: <input type="password" name="repeatPassword" /></p> + <p><input type="submit" /></p> +</form>
\ No newline at end of file diff --git a/ui/pwresetRequest.html b/ui/pwresetRequest.html new file mode 100644 index 0000000..a6c51b3 --- /dev/null +++ b/ui/pwresetRequest.html @@ -0,0 +1,6 @@ +<h1>Password reset</h1> + +<form action="{{ @BASE . @ALIASES.resetpw }}" method="post" class="tabform"> + <p><label for="pwreset_mail">E-Mail:</label> <input id="pwreset_mail" type="text" name="email" /></p> + <p class="formctrl"><input type="submit" /></p> +</form>
\ No newline at end of file diff --git a/ui/register.html b/ui/register.html new file mode 100644 index 0000000..8686600 --- /dev/null +++ b/ui/register.html @@ -0,0 +1,10 @@ +<h1>Register</h1> + +<form action="{{ @BASE . @ALIASES.register }}" method="post" class="tabform"> + <p><label for="reg_name">Name:</label> <input id="reg_name" type="text" name="name" value="" /></p> + <p><label for="reg_mail">E-Mail:</label> <input id="reg_mail" type="text" name="email" value="" /></p> + <p><label for="reg_pass">Password:</label> <input id="reg_pass" type="password" name="password" /></p> + <p><label for="reg_repeat">Repeat Password:</label> <input id="reg_repeat" type="password" name="repeatPassword" /></p> + <p class="formctrl"><input type="submit" /></p> +</form> +<p>Already have an account? Go to <a href="{{ @BASE . @ALIASES.login }}">Login</a>.</p>
\ No newline at end of file diff --git a/ui/settings.html b/ui/settings.html new file mode 100644 index 0000000..47f3b1b --- /dev/null +++ b/ui/settings.html @@ -0,0 +1,8 @@ +<h1>Account Settings</h1> + +<form action="{{ @BASE . @ALIASES.settings }}" method="post" class="tabform"> + <p><label for="accset_mail">New E-Mail Address:</label> <input id="accset_mail" type="text" name="email" value="{{ @email }}" /></p> + <p><label for="accset_newpw">New Password:</label> <input id="accset_newpw" type="password" name="password" /></p> + <p><label for="accset_repeatpw">Retype Password:</label> <input id="accset_repeatpw"type="password" name="repeatPassword" /></p> + <p class="formctrl"><input type="submit" /></p> +</form>
\ No newline at end of file diff --git a/ui/style.css b/ui/style.css new file mode 100644 index 0000000..18ae794 --- /dev/null +++ b/ui/style.css @@ -0,0 +1,306 @@ +@import url("DejaVu_Sans_ExtraLight/stylesheet.css"); +* { + font-family: sans-serif; +} +html, body { + padding: 0px; + margin: 0px; +} +body { + background: white; + font-size: 12pt; +} + +input[type="text"], input[type="password"] { + background: #fff; + border: 1px solid #666; + font-size: 12pt; + padding: 0.5ex 0ex 0.5ex; +} +input[type="text"]:hover, input[type="password"]:hover { + border-color: #000; +} +button, .fakebutton { + background: #eee; + border: 1px solid #ccc; + font-size: 12pt; + padding: 0.5ex 0ex 0.5ex; + cursor: pointer; +} +button:hover, .fakebutton:hover { + background-color: #ccc !important; +} +a.fakebutton { + color: black; + text-decoration: none; + display: inline-block; +} + +#headerwrap { + position: fixed; + top: 0px; + width: 100%; + background: #aaa; + color: #eee; + margin: 0px; + z-index: 1000; +} + +#header { + margin: 0px 7mm 0px; +} + +#header a { + display: inline-block; + color: #eee; + text-decoration: none; + border-bottom: 3px solid #bbb; + height: 100%; + vertical-align: middle; + padding: 0.5ex 0ex 0.5ex; +} +#header a:hover { + color: #fff; + border-bottom-color: #55c5ff !important; +} + +#userinfo { + position: absolute; + right: 7mm; +} +.username { + color: white; +} + +.bghelp { + z-index: -1; + background: #eee; + position: fixed; + margin: 0; + padding: 0; + top: 0; + bottom: 0; + left: 0; + width: 60mm; +} + +#lists { + position: absolute; + left: 0; + top: 0; + width: 60mm; + z-index: 1; + padding: 0mm; + background: #eee; +} + +#lists ul { + list-style: none; + margin: 3.5ex 0mm 3em; + padding: 0px; +} + +#lists ul li { + position: relative; + margin: 0; + padding: 0.5ex 3ex 0.5ex 1ex; +} +#lists ul li:hover { + background: #f8f8f8; +} +#lists ul li.active { + background: #fff; +} +#lists a { + color: #555; + text-decoration: none; +} +#lists a:hover { + color: black; +} +.listlink { + display: block; +} + + +.deletelist { + display: inline-block; + margin:0; + position: absolute; + top: 0; + bottom: 0; + right: 0; + width: 3ex; + border-left: 1px dotted #555; + text-align: center; + padding: 0.5ex 0ex 0.5ex 0ex; +} + +.deletelist:hover { + background: #ccc; +} + +#addlist { + position: fixed; + bottom: 0px; + width: 58mm; + background: #ddd; + padding: 1mm; + vertical-align: middle; +} +#addlist button { + position: absolute; + right: 1mm; + width: 7mm; + margin: 0mm; +} +#addlist input { + width: 100%; + margin: 0mm; + width: 50mm; +} + +#content { + padding: 4ex 2mm 2mm 63mm; +} + +h1 { + font-family: "DejaVu Sans ExtraLight", sans-serif; + font-weight: bold; + font-size: 16pt; +} + +h1 input.listname { + border: 1px solid #ccc; + font-size: 16pt; + margin: 0; + font-family: "DejaVu Sans ExtraLight", sans-serif; + font-weight: bold; +} +h1 input.listname:hover, h1 input.listname:focus { + border: 1px solid black; +} +h1 button { + background-color: #f7f7f7; + color: #ddd; + border: 1px solid #ddd; +} +h1:hover button { + background: #eee; + border: 1px solid #ccc; + color: black; +} + +ul.todolist { + list-style: none; + margin: 0; + padding: 0; +} +ul.todolist li { + margin: 0; + position: relative; + padding: 0.6ex 0 0.6ex; + border-top: 1px solid #f7f7f7; + min-height: 34px +} +ul.todolist li:first-child { + border-top: none; +} +ul.todolist form { + display: inline; +} +li.checked span.txt { + font-style: italic; + color: gray; + text-decoration: line-through; + vertical-align: middle; +} +ul.todolist span.btncluster { + vertical-align: middle; + position: absolute; + right: 0; +} +ul.todolist span.btncluster button { + background: #fbfbfb; + border: 1px solid #f2f2f2; + color: #bbb; +} +ul.todolist li:hover span.btncluster button { + background: #eee; + border: 1px solid #ccc; + color: black; +} + +.chkbtn { + padding: 0; + cursor: pointer; + width: 20px; + height: 20px; + vertical-align: middle; +} +.chkbtn.checked { + background-image: url(img/sprites.png); +} +.chkbtn input { + display: none; +} + +.notify { + margin: -100px auto 1mm; + width: 50%; + text-align: center; + color: white; + padding: 100px 5px 5px 35px; + position: relative; +} + +.notify .icon { + display: block; + width: 20px; + height: 100%; + position: absolute; + left: 5px; + top: 45px; + background-size: 80px 20px; + background-repeat: no-repeat; + background-image: url(img/sprites.png); +} + +.success {background: #2a0;} +.success .icon {background-position: left -20px top 50%;} +.info {background: #48f;} +.info .icon {background-position: left -40px top 50%;} +.error {background: #f40;} +.error .icon {background-position: left -60px top 50%;} + +.delchecked { + text-align: right; +} + +.empty { + font-style: italic; + margin: 2mm; +} +.nolist { + font-style: italic; + font-size: 18pt; + color: #999; + text-align: center; + position: absolute; + top: 40%; +} + +form.tabform { + display: table; +} +form.tabform p { + display: table-row; +} +form.tabform input, form.tabform label { + display: table-cell; + margin: 1mm; +} +form.tabform p.formctrl { + display: block; + margin: 0; +} diff --git a/ui/welcome.html b/ui/welcome.html new file mode 100644 index 0000000..3b110bb --- /dev/null +++ b/ui/welcome.html @@ -0,0 +1,3 @@ +<h1>Welcome</h1> + +<p>Foo!</p>
\ No newline at end of file |