├── LICENSE.txt ├── README.md ├── client └── main.go ├── config.json ├── config └── config.go ├── db ├── files.go ├── tasks.go └── user.go ├── files └── README.md ├── install.sh ├── main.go ├── public └── static │ ├── css │ ├── bootstrap-glyphicons.css │ ├── bootstrap.min.css │ ├── font-awesome.min.css │ ├── jquery-ui.min.css │ ├── sidebar-bootstrap.css │ ├── sidebar.css │ └── styles.css │ ├── fonts │ ├── glyphiconshalflings-regular.eot │ ├── glyphiconshalflings-regular.otf │ ├── glyphiconshalflings-regular.svg │ ├── glyphiconshalflings-regular.ttf │ └── glyphiconshalflings-regular.woff │ └── js │ ├── app.js │ ├── bootstrap.min.js │ ├── hammer.min.js │ ├── jquery-ui.min.js │ ├── jquery.min.js │ ├── script.js │ ├── sidebar.js │ ├── vue-resource.js │ ├── vue-router.js │ └── vue.js ├── schema.sql ├── sessions └── sessions.go ├── templates ├── _foot.html ├── _head.html └── tasks.html ├── types └── types.go ├── utils └── utils.go └── views ├── api.go ├── api_sessions.go └── views.go /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2015 Suraj Patil 5 | 6 | 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: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | 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. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tasks 2 | 3 | Tasks is a simplistic Go webapp to manage tasks, I built this tool to manage tasks which I wanted to do, there are many good kanban style boards, but I felt they were a bit too heavyweight for my taste. Also I wanted to learn the Go webapp development. 4 | 5 | How to use? 6 | ================== 7 | Via script: `bash install.sh` 8 | 9 | This will generate the binary and set up the database. If you want, you can copy the binary and the public folder into a folder of your choice. 10 | 11 | Manually: 12 | 13 | 1. `go get github.com/thewhitetulip/Tasks` 14 | 1. change dir to the respective folder and create the db file: `cat schema.sql | sqlite3 tasks.db` 15 | 1. run `go build` 16 | 1. `./Tasks` 17 | 1. open [localhost:8081](http://localhost:8081) 18 | 19 | You can change the port in the [config](https://github.com/thewhitetulip/Tasks/blob/master/config.json) file 20 | 21 | ##Features 22 | 23 | 1. Add, update, delete task. 24 | 2. Search tasks, the query is highlighted in the search results page. 25 | 3. Github flavoured markdown, which enables us for using a task list, advanced syntax highlighting and much more. 26 | 4. Supports file upload, randomizes the file name, stores the user given filename in a db and works on the randomized file name for security reasons. 27 | 5. Priorities are assigned, High = 3, medium = 2 and low = 1, sorting is done on priority descending and created date ascending. 28 | 6. Categories are supported, you can add tasks to different categories. 29 | 1. Ability to hide a task from the timeline. 30 | 1. For a task list, shows 6 out of 8 tasks completed. 31 | 1. Single click install, just run the install.sh file. 32 | 33 | 34 | ##### Book 35 | I am learning writing webapps with Go as I build this application, I took to writing an introductory book about [building webapps in Go](https://github.com/thewhitetulip/web-dev-golang-anti-textbook) because I faced a lot of problems while learning how to write webapps in Go, it, the book strives to teach by practical examples. You are welcome to contribute to the book. 36 | 37 | #Screenshots 38 | The Home Page 39 | 40 | ![Home Page](https://github.com/thewhitetulip/Tasks/blob/master/screenshots/FrontEnd.png) 41 | 42 | Add Task dialog 43 | 44 | ![Add Task](https://github.com/thewhitetulip/Tasks/blob/master/screenshots/FrontEnd-Add%20task.png) 45 | 46 | Navigation drawer 47 | 48 | ![Navigation Drawer](https://github.com/thewhitetulip/Tasks/blob/master/screenshots/FrontEnd%20Navigation%20Drawer.png) 49 | 50 | #License 51 | 52 | The MIT License (MIT) 53 | 54 | Copyright (c) 2015 Suraj Patil 55 | 56 | Permission is hereby granted, free of charge, to any person obtaining a copy 57 | of this software and associated documentation files (the "Software"), to deal 58 | in the Software without restriction, including without limitation the rights 59 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 60 | copies of the Software, and to permit persons to whom the Software is 61 | furnished to do so, subject to the following conditions: 62 | 63 | The above copyright notice and this permission notice shall be included in all 64 | copies or substantial portions of the Software. 65 | 66 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 67 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 68 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 69 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 70 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 71 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 72 | SOFTWARE. 73 | -------------------------------------------------------------------------------- /client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | 11 | "github.com/thewhitetulip/Tasks-vue/types" 12 | ) 13 | 14 | func main() { 15 | baseURL := "http://127.0.0.1:8080/" 16 | 17 | usernamePwd := url.Values{} 18 | usernamePwd.Set("username", "suraj") 19 | usernamePwd.Set("password", "suraj") 20 | 21 | resp, err := http.PostForm(baseURL+"api/get-token/", usernamePwd) 22 | if err != nil { 23 | fmt.Println("Is the server running?") 24 | os.Exit(1) 25 | } else { 26 | fmt.Println("response received") 27 | } 28 | 29 | defer resp.Body.Close() 30 | body, err := ioutil.ReadAll(resp.Body) 31 | if err != nil { 32 | fmt.Println("Error reading body") 33 | } else { 34 | fmt.Println("Token received") 35 | } 36 | token := string(body) 37 | 38 | client := &http.Client{} 39 | req, err := http.NewRequest("GET", baseURL+"api/get-task/", nil) 40 | 41 | if err != nil { 42 | fmt.Println("Unable to form a GET /api/get-task/") 43 | } 44 | 45 | req.Header.Add("Token", token) 46 | resp, err = client.Do(req) 47 | 48 | if (err != nil) || (resp.StatusCode != 200) { 49 | fmt.Println("Something went wrong in the getting a response") 50 | } 51 | 52 | defer resp.Body.Close() 53 | body, err = ioutil.ReadAll(resp.Body) 54 | fmt.Println(string(body)) 55 | 56 | var context types.Tasks 57 | 58 | if err := json.Unmarshal(body, &context); err != nil { 59 | panic(err) 60 | } 61 | 62 | fmt.Println(context) 63 | 64 | } 65 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerPort": ":9081" 3 | } 4 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Configuration package is used to read the configuration file 3 | config.json which stores the server port for current implementation 4 | { 5 | "ServerPort": ":8081" 6 | } 7 | */ 8 | package config 9 | 10 | import ( 11 | "encoding/json" 12 | "io/ioutil" 13 | "log" 14 | ) 15 | 16 | //Configuration Stores the main configuration for the application 17 | type Configuration struct { 18 | ServerPort string 19 | } 20 | 21 | var err error 22 | var config Configuration 23 | 24 | //ReadConfig will read the configuration json file to read the parameters 25 | //which will be passed in the config file 26 | func ReadConfig(fileName string) (Configuration, error) { 27 | configFile, err := ioutil.ReadFile(fileName) 28 | if err != nil { 29 | log.Print("Unable to read config file, switching to flag mode") 30 | return Configuration{}, err 31 | } 32 | //log.Print(configFile) 33 | err = json.Unmarshal(configFile, &config) 34 | if err != nil { 35 | log.Print("Invalid JSON, expecting port from command line flag") 36 | return Configuration{}, err 37 | } 38 | return config, nil 39 | } 40 | -------------------------------------------------------------------------------- /db/files.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | /* 4 | stores the functions related to file IO and category 5 | */ 6 | import ( 7 | "log" 8 | 9 | "github.com/thewhitetulip/Tasks-vue/types" 10 | ) 11 | 12 | // AddFile is used to add the md5 of a file name which is uploaded to our application 13 | // this will enable us to randomize the URL without worrying about the file names 14 | func AddFile(fileName, token, username string) error { 15 | userID, err := GetUserID(username) 16 | if err != nil { 17 | return err 18 | } 19 | err = taskQuery("insert into files values(?,?,?,datetime())", fileName, token, userID) 20 | return err 21 | } 22 | 23 | // GetFileName is used to fetch the name according to the md5 checksum from the db 24 | func GetFileName(token string) (string, error) { 25 | sql := "select name from files where autoName=?" 26 | var fileName string 27 | rows := database.query(sql, fileName) 28 | defer rows.Close() 29 | if rows.Next() { 30 | err := rows.Scan(&fileName) 31 | if err != nil { 32 | log.Println(err) 33 | return "", err 34 | } 35 | } 36 | if err != nil { 37 | return "", err 38 | } 39 | 40 | return fileName, nil 41 | } 42 | 43 | //GetCategories will return the list of categories to be 44 | //rendered in the template 45 | func GetCategories(username string) ([]types.Category, error) { 46 | var categories []types.Category 47 | var category types.Category 48 | 49 | userID, err := GetUserID(username) 50 | if err != nil { 51 | return categories, nil 52 | } 53 | stmt := "select c.id, c.name, count(*) from category c left outer join task t join status s on c.id = t.cat_id and t.task_status_id=s.id where s.status='PENDING' and c.user_id=? group by name union select c.id, name, 0 from category c, user u where c.user_id=? and name not in (select distinct name from task t join category c join status s on s.id = t.task_status_id and t.cat_id = c.id and s.status='PENDING' and c.user_id=?)" 54 | rows := database.query(stmt, userID, userID, userID) 55 | 56 | defer rows.Close() 57 | for rows.Next() { 58 | err := rows.Scan(&category.ID, &category.Name, &category.TaskCount) 59 | if err != nil { 60 | return categories, err 61 | } 62 | categories = append(categories, category) 63 | } 64 | return categories, nil 65 | } 66 | 67 | //AddCategory is used to add the task in the database 68 | func AddCategory(username, category string) error { 69 | userID, err := GetUserID(username) 70 | if err != nil { 71 | return nil 72 | } 73 | log.Println("executing query to add category") 74 | err = taskQuery("insert into category(name, user_id) values(?,?)", category, userID) 75 | return err 76 | } 77 | 78 | // GetCategoryByName will return the ID of that category passed as args 79 | // used while inserting tasks into the table 80 | func GetCategoryByName(username, category string) int { 81 | stmt := "select id from category where name=? and user_id = (select id from user where username=?)" 82 | rows := database.query(stmt, category, username) 83 | var categoryID int 84 | defer rows.Close() 85 | for rows.Next() { 86 | err := rows.Scan(&categoryID) 87 | if err != nil { 88 | log.Println(err) 89 | } 90 | } 91 | return categoryID 92 | } 93 | 94 | //DeleteCategoryByName will be used to delete a category from the category page 95 | func DeleteCategoryByName(username, category string) error { 96 | //first we delete entries from task and then from category 97 | categoryID := GetCategoryByName(username, category) 98 | userID, err := GetUserID(username) 99 | if err != nil { 100 | return err 101 | } 102 | query := "update task set cat_id = null where id =? and user_id = ?" 103 | err = taskQuery(query, categoryID, userID) 104 | if err == nil { 105 | err = taskQuery("delete from category where id=? and user_id=?", categoryID, userID) 106 | if err != nil { 107 | return err 108 | } 109 | } 110 | return err 111 | } 112 | 113 | //UpdateCategoryByName will be used to delete a category from the category page 114 | func UpdateCategoryByName(username, oldName, newName string) error { 115 | userID, err := GetUserID(username) 116 | if err != nil { 117 | return err 118 | } 119 | query := "update category set name = ? where name=? and user_id=?" 120 | log.Println(query) 121 | err = taskQuery(query, newName, oldName, userID) 122 | return err 123 | } 124 | 125 | //DeleteCommentByID will actually delete the comment from db 126 | func DeleteCommentByID(username string, id int) error { 127 | userID, err := GetUserID(username) 128 | if err != nil { 129 | return err 130 | } 131 | query := "delete from comments where id=? and user_id = ?" 132 | err = taskQuery(query, id, userID) 133 | return err 134 | } 135 | -------------------------------------------------------------------------------- /db/tasks.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | /* 4 | Stores the database functions related to tasks like 5 | GetTaskByID(id int) 6 | GetTasks(status string) 7 | DeleteAll() 8 | */ 9 | 10 | import ( 11 | "database/sql" 12 | "log" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | _ "github.com/mattn/go-sqlite3" //we want to use sqlite natively 18 | "github.com/thewhitetulip/Tasks-vue/types" 19 | ) 20 | 21 | var database Database 22 | var taskStatus map[string]int 23 | var err error 24 | 25 | //Database encapsulates database 26 | type Database struct { 27 | db *sql.DB 28 | } 29 | 30 | //Begins a transaction 31 | func (db Database) begin() (tx *sql.Tx) { 32 | tx, err := db.db.Begin() 33 | if err != nil { 34 | log.Println(err) 35 | return nil 36 | } 37 | return tx 38 | } 39 | 40 | func (db Database) prepare(q string) (stmt *sql.Stmt) { 41 | stmt, err := db.db.Prepare(q) 42 | if err != nil { 43 | log.Println(err) 44 | return nil 45 | } 46 | return stmt 47 | } 48 | 49 | func (db Database) query(q string, args ...interface{}) (rows *sql.Rows) { 50 | rows, err := db.db.Query(q, args...) 51 | if err != nil { 52 | log.Println(err) 53 | return nil 54 | } 55 | return rows 56 | } 57 | 58 | func init() { 59 | database.db, err = sql.Open("sqlite3", "./tasks.db") 60 | taskStatus = map[string]int{"COMPLETE": 1, "PENDING": 2, "DELETED": 3} 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | } 65 | 66 | //Close function closes this database connection 67 | func Close() { 68 | database.db.Close() 69 | } 70 | 71 | //GetTasks retrieves all the tasks depending on the 72 | //status pending or trashed or completed 73 | func GetTasks(username, status, category string) (types.Context, error) { 74 | log.Println("getting tasks for ", status) 75 | var tasks []types.Task 76 | var task types.Task 77 | var TaskCreated time.Time 78 | var context types.Context 79 | var getTaskSQL string 80 | var rows *sql.Rows 81 | 82 | comments, err := GetComments(username) 83 | 84 | if err != nil { 85 | return context, err 86 | } 87 | 88 | basicSQL := "select t.id, title, content, created_date, priority, case when c.name is null then 'NA' else c.name end from task t, status s, user u left outer join category c on c.id=t.cat_id where u.username=? and s.id=t.task_status_id and u.id=t.user_id " 89 | if category == "" { 90 | switch status { 91 | case "pending": 92 | getTaskSQL = basicSQL + " and s.status='PENDING' and t.hide!=1" 93 | case "deleted": 94 | getTaskSQL = basicSQL + " and s.status='DELETED' and t.hide!=1" 95 | case "completed": 96 | getTaskSQL = basicSQL + " and s.status='COMPLETE' and t.hide!=1" 97 | } 98 | 99 | getTaskSQL += " order by t.created_date asc" 100 | 101 | rows = database.query(getTaskSQL, username, username) 102 | } else { 103 | status = category 104 | //This is a special case for showing tasks with null categories, we do a union query 105 | if category == "UNCATEGORIZED" { 106 | getTaskSQL = "select t.id, title, content, created_date, priority, 'UNCATEGORIZED' from task t, status s, user u where u.username=? and s.id=t.task_status_id and u.id=t.user_id and t.cat_id=0 and s.status='PENDING' order by priority desc, created_date asc, finish_date asc" 107 | rows, err = database.db.Query(getTaskSQL, username) 108 | } else { 109 | getTaskSQL = basicSQL + " and name = ? and s.status='PENDING' order by priority desc, created_date asc, finish_date asc" 110 | rows, err = database.db.Query(getTaskSQL, username, category) 111 | } 112 | 113 | if err != nil { 114 | log.Println("tasks.go: something went wrong while getting query fetch tasks by category") 115 | } 116 | } 117 | 118 | defer rows.Close() 119 | for rows.Next() { 120 | task = types.Task{} 121 | 122 | err = rows.Scan(&task.Id, &task.Title, &task.Content, &TaskCreated, &task.Priority, &task.Category) 123 | 124 | taskCompleted := 0 125 | totalTasks := 0 126 | 127 | if strings.HasPrefix(task.Content, "- [") { 128 | for _, value := range strings.Split(task.Content, "\n") { 129 | if strings.HasPrefix(value, "- [x]") { 130 | taskCompleted += 1 131 | } 132 | totalTasks += 1 133 | } 134 | task.CompletedMsg = strconv.Itoa(taskCompleted) + " complete out of " + strconv.Itoa(totalTasks) 135 | } 136 | 137 | // TaskContent = strings.Replace(TaskContent, "\n", "
", -1) 138 | if err != nil { 139 | log.Println(err) 140 | } 141 | 142 | if comments[task.Id] != nil { 143 | task.Comments = comments[task.Id] 144 | } 145 | 146 | TaskCreated = TaskCreated.Local() 147 | // if task.Priority != "1" { // if priority is not 1 then calculate, else why bother? 148 | // CurrentTime := time.Now().Local() 149 | // diff := CurrentTime.Sub(TaskCreated).Hours() 150 | // if diff > 168 { 151 | // task.IsOverdue = true // If one week then overdue by default 152 | // } 153 | // } 154 | task.Created = TaskCreated.Format("Jan 2 2006") 155 | 156 | tasks = append(tasks, task) 157 | } 158 | context = types.Context{Tasks: tasks, Navigation: status} 159 | return context, nil 160 | } 161 | 162 | //GetTaskByID function gets the tasks from the ID passed to the function, used to populate EditTask 163 | func GetTaskByID(username string, id int) (types.Context, error) { 164 | var tasks []types.Task 165 | var task types.Task 166 | 167 | getTaskSQL := "select t.id, t.title, t.content, t.priority, t.hide, c.name from task t join user u left outer join category c where c.id = t.cat_id and t.user_id=u.id and t.id=? and u.username=? union select t.id, t.title, t.content, t.priority, t.hide, 'UNCATEGORIZED' from task t join user u where t.user_id=u.id and t.cat_id=0 ;" 168 | 169 | rows := database.query(getTaskSQL, id, username) 170 | defer rows.Close() 171 | if rows.Next() { 172 | err := rows.Scan(&task.Id, &task.Title, &task.Content, &task.Priority, &task.IsHidden, &task.Category) 173 | if err != nil { 174 | log.Println(err) 175 | //send email to respective people 176 | } 177 | } 178 | tasks = append(tasks, task) 179 | context := types.Context{Tasks: tasks, Navigation: "edit"} 180 | return context, nil 181 | } 182 | 183 | //TrashTask is used to delete the task 184 | func TrashTask(username string, id int) error { 185 | err := taskQuery("update task set task_status_id=?,last_modified_at=datetime() where user_id=(select id from user where username=?) and id=?", taskStatus["DELETED"], username, id) 186 | return err 187 | } 188 | 189 | //CompleteTask is used to mark tasks as complete 190 | func CompleteTask(username string, id int) error { 191 | err := taskQuery("update task set task_status_id=?, finish_date=datetime(),last_modified_at=datetime() where id=? and user_id=(select id from user where username=?) ", taskStatus["COMPLETE"], id, username) 192 | return err 193 | } 194 | 195 | //DeleteAll is used to empty the trash 196 | func DeleteAll(username string) error { 197 | err := taskQuery("delete from task where task_status_id=? where user_id=(select id from user where username=?)", taskStatus["DELETED"], username) 198 | return err 199 | } 200 | 201 | //RestoreTask is used to restore tasks from the Trash 202 | func RestoreTask(username string, id int) error { 203 | err := taskQuery("update task set task_status_id=?,last_modified_at=datetime(),finish_date=null where id=? and user_id=(select id from user where username=?)", taskStatus["PENDING"], id, username) 204 | return err 205 | } 206 | 207 | //RestoreTaskFromComplete is used to restore tasks from the Trash 208 | func RestoreTaskFromComplete(username string, id int) error { 209 | err := taskQuery("update task set finish_date=null,last_modified_at=datetime(), task_status_id=? where id=? and user_id=(select id from user where username=?)", taskStatus["PENDING"], id, username) 210 | return err 211 | } 212 | 213 | //DeleteTask is used to delete the task from the database 214 | func DeleteTask(username string, id int) error { 215 | err := taskQuery("delete from task where id = ? and user_id=(select id from user where username=?)", id, username) 216 | return err 217 | } 218 | 219 | //AddTask is used to add the task in the database 220 | //TODO: add dueDate feature later 221 | func AddTask(title, content, category string, taskPriority int, username string, hidden int) error { 222 | log.Println("AddTask: started function") 223 | var err error 224 | /*var timeDueDate time.Time 225 | if duedate != "" { 226 | timeDueDate, err = time.Parse("12/31/2016", duedate) 227 | if err != nil { 228 | log.Fatal(err) 229 | } 230 | }*/ 231 | userID, err := GetUserID(username) 232 | if err != nil && (title != "" || content != "") { 233 | return err 234 | } 235 | 236 | if category == "" { 237 | err = taskQuery("insert into task(title, content, priority, task_status_id, created_date, last_modified_at, user_id,hide) values(?,?,?,?,datetime(), datetime(),?,?)", title, content, taskPriority, taskStatus["PENDING"], userID, hidden) 238 | } else { 239 | categoryID := GetCategoryByName(username, category) 240 | err = taskQuery("insert into task(title, content, priority, created_date, last_modified_at, cat_id, task_status_id, user_id,hide) values(?,?,?,datetime(), datetime(), ?,?,?,?)", title, content, taskPriority, categoryID, taskStatus["PENDING"], userID, hidden) 241 | } 242 | return err 243 | } 244 | 245 | //GetCategoryIDByName will return the category ID for the category, used in the edit task 246 | //function where we need to be able to update the categoryID of the task 247 | func GetCategoryIDByName(username string, category string) int { 248 | var categoryID int 249 | getTaskSQL := "select c.id from category c , user u where u.id = c.user_id and name=? and u.username=?" 250 | 251 | rows := database.query(getTaskSQL, category, username) 252 | defer rows.Close() 253 | if rows.Next() { 254 | err := rows.Scan(&categoryID) 255 | if err != nil { 256 | log.Println(err) 257 | //send email to respective people 258 | } 259 | } 260 | 261 | return categoryID 262 | } 263 | 264 | //UpdateTask is used to update the tasks in the database 265 | func UpdateTask(id int, title, content, category string, priority int, username string, hidden int) error { 266 | categoryID := GetCategoryIDByName(username, category) 267 | userID, err := GetUserID(username) 268 | if err != nil { 269 | return err 270 | } 271 | err = taskQuery("update task set title=?, content=?, cat_id=?, priority = ? where id=? and user_id=?", title, content, categoryID, priority, id, userID) 272 | return err 273 | } 274 | 275 | //taskQuery encapsulates running multiple queries which don't do much things 276 | func taskQuery(sql string, args ...interface{}) error { 277 | log.Print("inside task query") 278 | SQL := database.prepare(sql) 279 | tx := database.begin() 280 | _, err = tx.Stmt(SQL).Exec(args...) 281 | if err != nil { 282 | log.Println("taskQuery: ", err) 283 | tx.Rollback() 284 | } else { 285 | err = tx.Commit() 286 | if err != nil { 287 | log.Println(err) 288 | return err 289 | } 290 | log.Println("Commit successful") 291 | } 292 | return err 293 | } 294 | 295 | //SearchTask is used to return the search results depending on the query 296 | func SearchTask(username, query string) (types.Context, error) { 297 | var tasks []types.Task 298 | var task types.Task 299 | var TaskCreated time.Time 300 | var context types.Context 301 | 302 | comments, err := GetComments(username) 303 | if err != nil { 304 | log.Println("SearchTask: something went wrong in finding comments") 305 | } 306 | 307 | userID, err := GetUserID(username) 308 | if err != nil { 309 | return context, err 310 | } 311 | 312 | stmt := "select t.id, title, content, created_date, priority, c.name from task t, category c where t.user_id=? and c.id = t.cat_id and (title like '%" + query + "%' or content like '%" + query + "%') order by created_date desc" 313 | 314 | rows := database.query(stmt, userID, query, query) 315 | defer rows.Close() 316 | for rows.Next() { 317 | err := rows.Scan(&task.Id, &task.Title, &task.Content, &TaskCreated, &task.Priority, &task.Category) 318 | if err != nil { 319 | log.Println(err) 320 | } 321 | 322 | if comments[task.Id] != nil { 323 | task.Comments = comments[task.Id] 324 | } 325 | 326 | task.Title = strings.Replace(task.Title, query, ""+query+"", -1) 327 | task.Content = strings.Replace(task.Content, query, ""+query+"", -1) 328 | 329 | TaskCreated = TaskCreated.Local() 330 | CurrentTime := time.Now().Local() 331 | week := TaskCreated.AddDate(0, 0, 7) 332 | 333 | if (week.String() < CurrentTime.String()) && (task.Priority != "1") { 334 | task.IsOverdue = true // If one week then overdue by default 335 | } 336 | task.Created = TaskCreated.Format("Jan 2 2006") 337 | 338 | tasks = append(tasks, task) 339 | } 340 | context = types.Context{Tasks: tasks, Search: query, Navigation: "search"} 341 | return context, nil 342 | } 343 | 344 | //GetComments is used to get comments, all of them. 345 | //We do not want 100 different pages to show tasks, we want to use as few pages as possible 346 | //so we are going to populate everything on the damn home pages 347 | func GetComments(username string) (map[int][]types.Comment, error) { 348 | commentMap := make(map[int][]types.Comment) 349 | 350 | var taskID int 351 | var comment types.Comment 352 | var created time.Time 353 | 354 | userID, err := GetUserID(username) 355 | if err != nil { 356 | return commentMap, err 357 | } 358 | stmt := "select c.id, c.taskID, c.content, c.created, u.username from comments c, task t, user u where t.id=c.taskID and c.user_id=t.user_id and t.user_id=u.id and u.id=?" 359 | rows := database.query(stmt, userID) 360 | 361 | defer rows.Close() 362 | for rows.Next() { 363 | err := rows.Scan(&comment.ID, &taskID, &comment.Content, &created, &comment.Username) 364 | if err != nil { 365 | return commentMap, err 366 | } 367 | // comment.Content = string(md.Markdown([]byte(comment.Content))) ## have to fix the

issue markdown support 368 | created = created.Local() 369 | comment.Created = created.Format("Jan 2 2006 15:04:05") 370 | commentMap[taskID] = append(commentMap[taskID], comment) 371 | } 372 | return commentMap, nil 373 | } 374 | 375 | // AddComments will be used to add comments in the database and return the added comment's ID and created date 376 | // along with the error if there is any. It will be sent back to the front end for display. 377 | func AddComments(username string, id int, content string) (types.Comment, error) { 378 | var comment types.Comment 379 | 380 | userID, err := GetUserID(username) 381 | if err != nil { 382 | return comment, err 383 | } 384 | stmt := "insert into comments(taskID, content, created, user_id) values (?,?,datetime(),?)" 385 | 386 | created := time.Now().Local().Format("Jan 2 2006 15:04:05") 387 | 388 | err = taskQuery(stmt, id, content, userID) 389 | 390 | if err != nil { 391 | return comment, err 392 | } 393 | 394 | stmt = "select id, created from comments where taskID=? and content=? and user_id=?" 395 | rows := database.query(stmt, id, content, userID) 396 | 397 | defer rows.Close() 398 | 399 | for rows.Next() { 400 | err := rows.Scan(&comment.ID, &comment.Created) 401 | if err != nil { 402 | log.Println("Error ", err) 403 | } 404 | } 405 | comment.Created = created 406 | 407 | comment.Username = username 408 | comment.Content = content 409 | 410 | log.Println("added comment to task ID ", id) 411 | 412 | return comment, nil 413 | } 414 | -------------------------------------------------------------------------------- /db/user.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import "log" 4 | 5 | //CreateUser will create a new user, take as input the parameters and 6 | //insert it into database 7 | func CreateUser(username, password, email string) error { 8 | err := taskQuery("insert into user(username, password, email) values(?,?,?)", username, password, email) 9 | return err 10 | } 11 | 12 | //ValidUser will check if the user exists in db and if exists if the username password 13 | //combination is valid 14 | func ValidUser(username, password string) bool { 15 | var passwordFromDB string 16 | userSQL := "select password from user where username=?" 17 | log.Print("validating user ", username) 18 | rows := database.query(userSQL, username) 19 | 20 | defer rows.Close() 21 | if rows.Next() { 22 | err := rows.Scan(&passwordFromDB) 23 | if err != nil { 24 | return false 25 | } 26 | } 27 | //If the password matches, return true 28 | if password == passwordFromDB { 29 | return true 30 | } 31 | //by default return false 32 | return false 33 | } 34 | 35 | //GetUserID will get the user's ID from the database 36 | func GetUserID(username string) (int, error) { 37 | var userID int 38 | userSQL := "select id from user where username=?" 39 | rows := database.query(userSQL, username) 40 | 41 | defer rows.Close() 42 | if rows.Next() { 43 | err := rows.Scan(&userID) 44 | if err != nil { 45 | return -1, err 46 | } 47 | } 48 | return userID, nil 49 | } 50 | -------------------------------------------------------------------------------- /files/README.md: -------------------------------------------------------------------------------- 1 | Will store the files uploaded to the application 2 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | echo "changing directory to Tasks" 2 | cd $GOPATH/src/github.com/thewhitetulip/Tasks 3 | echo "creating table" 4 | cat schema.sql | sqlite3 tasks.db 5 | echo "building the go binary" 6 | go build -o Tasks 7 | 8 | echo "starting the binary" 9 | ./Tasks 10 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /** 4 | * This is the main file for the Task application 5 | * License: MIT 6 | **/ 7 | import ( 8 | "flag" 9 | "log" 10 | "net/http" 11 | "strings" 12 | 13 | "github.com/thewhitetulip/Tasks-vue/config" 14 | "github.com/thewhitetulip/Tasks-vue/views" 15 | 16 | "github.com/gorilla/mux" 17 | ) 18 | 19 | func main() { 20 | values, err := config.ReadConfig("config.json") 21 | var port *string 22 | 23 | if err != nil { 24 | port = flag.String("port", "", "IP address") 25 | flag.Parse() 26 | 27 | // User is expected to give :8080 like input, if they give 8080 28 | // we'll append the required ':'. 29 | if !strings.HasPrefix(*port, ":") { 30 | *port = ":" + *port 31 | log.Println("port is " + *port) 32 | } 33 | 34 | values.ServerPort = *port 35 | } 36 | 37 | views.PopulateTemplates() 38 | 39 | r := mux.NewRouter() 40 | 41 | http.Handle("/static/", http.FileServer(http.Dir("public"))) 42 | 43 | r.HandleFunc("/task/", views.GetTasksFuncAPI).Methods("GET") 44 | r.HandleFunc("/task/", views.AddTaskFuncAPI).Methods("PUT") 45 | r.HandleFunc("/task/", views.UpdateTaskFuncAPI).Methods("POST") 46 | r.HandleFunc("/task/{id}", views.TrashTaskFuncAPI).Methods("DELETE") 47 | r.HandleFunc("/deleted/", views.GetDeletedTaskFuncAPI).Methods("GET") 48 | 49 | r.HandleFunc("/completed/", views.GetCompletedTaskFuncAPI).Methods("GET") 50 | r.HandleFunc("/categories/", views.GetCategoryFuncAPI).Methods("GET") 51 | 52 | r.HandleFunc("/category/{category}", views.ShowCategoryFuncAPI).Methods("GET") 53 | r.HandleFunc("/category/{category}", views.DeleteCategoryFuncAPI).Methods("DELETE") 54 | r.HandleFunc("/complete-task/{id}", views.CompleteTaskFuncAPI).Methods("GET") 55 | r.HandleFunc("/incomplete-task/{id}", views.RestoreFromCompleteFuncAPI).Methods("GET") 56 | r.HandleFunc("/restore-task/{id}", views.RestoreTaskFuncAPI).Methods("GET") 57 | r.HandleFunc("/", views.Home).Methods("GET") 58 | 59 | r.HandleFunc("/comment/", views.AddCommentFuncAPI).Methods("PUT") 60 | r.HandleFunc("/comment/{id}", views.DeleteCommentFuncAPI).Methods("DELETE") 61 | r.HandleFunc("/category/", views.AddCategoryFuncAPI).Methods("PUT") 62 | r.HandleFunc("/category/{category}", views.UpdateCategoryFuncAPI).Methods("POST") 63 | 64 | //Login logout 65 | r.HandleFunc("/login/", views.LoginFuncAPI).Methods("POST", "GET") 66 | r.HandleFunc("/logout/", views.RequiresLogin(views.LogoutFuncAPI)).Methods("GET") 67 | r.HandleFunc("/signup/", views.SignUpFuncAPI).Methods("PUT") 68 | 69 | //these handlers perform action like delete, mark as complete etc 70 | // http.HandleFunc("/files/", views.RequiresLogin(views.UploadedFileHandler)) 71 | // http.HandleFunc("/search/", views.RequiresLogin(views.SearchTaskFunc)) 72 | http.Handle("/", r) 73 | log.Println("running server on ", values.ServerPort) 74 | log.Fatal(http.ListenAndServe(values.ServerPort, nil)) 75 | } 76 | -------------------------------------------------------------------------------- /public/static/css/bootstrap-glyphicons.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphiconshalflings-regular.eot');src:url('../fonts/glyphiconshalflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphiconshalflings-regular.woff') format('woff'),url('../fonts/glyphiconshalflings-regular.ttf') format('truetype'),url('../fonts/glyphiconshalflings-regular.svg#glyphicons_halflingsregular') format('svg')}.glyphicon:before{font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-plus:before{content:"\002b"}.glyphicon-minus:before{content:"\2212"}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse:before{content:"\e159"}.glyphicon-collapse-top:before{content:"\e160"} 2 | /* This beautiful CSS-File has been crafted with LESS (lesscss.org) and compiled by simpLESS (wearekiss.com/simpless) */ 3 | -------------------------------------------------------------------------------- /public/static/css/jquery-ui.min.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.11.1 - 2014-08-13 2 | * http://jqueryui.com 3 | * Includes: core.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, draggable.css, menu.css, progressbar.css, resizable.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css 4 | * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=glass&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=glass&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=diagonals_thick&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=diagonals_thick&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=flat&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px 5 | * Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ 6 | 7 | .ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;min-height:0;font-size:100%}.ui-accordion .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-icons .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-header .ui-accordion-header-icon{position:absolute;left:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-dialog{overflow:hidden;position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:none}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{position:relative;margin:0;padding:3px 1em 3px .4em;cursor:pointer;min-height:0;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-button{display:inline-block;overflow:hidden;position:relative;text-decoration:none;cursor:pointer}.ui-selectmenu-button span.ui-icon{right:0.5em;left:auto;margin-top:-8px;position:absolute;top:50%}.ui-selectmenu-button span.ui-selectmenu-text{text-align:left;padding:0.4em 2.1em 0.4em 1em;display:block;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:22px}.ui-spinner-button{width:16px;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top:none;border-bottom:none;border-right:none}.ui-spinner .ui-icon{position:absolute;margin-top:-8px;top:50%;left:0}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-spinner .ui-icon-triangle-1-s{background-position:-65px -16px}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #ddd;background:#eee url("images/ui-bg_highlight-soft_100_eeeeee_1x100.png") 50% top repeat-x;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #e78f08;background:#f6a828 url("images/ui-bg_gloss-wave_35_f6a828_500x100.png") 50% 50% repeat-x;color:#fff;font-weight:bold}.ui-widget-header a{color:#fff}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #ccc;background:#f6f6f6 url("images/ui-bg_glass_100_f6f6f6_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#1c94c4}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#1c94c4;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #fbcb09;background:#fdf5ce url("images/ui-bg_glass_100_fdf5ce_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#c77405}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#c77405;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #fbd850;background:#fff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#eb8f00}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#eb8f00;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fed22f;background:#ffe45c url("images/ui-bg_highlight-soft_75_ffe45c_1x100.png") 50% top repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#b81900 url("images/ui-bg_diagonals-thick_18_b81900_40x40.png") 50% 50% repeat;color:#fff}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#fff}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#fff}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-default .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-active .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("images/ui-icons_228ef1_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_ffd27a_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#666 url("images/ui-bg_diagonals-thick_20_666666_40x40.png") 50% 50% repeat;opacity:.5;filter:Alpha(Opacity=50)}.ui-widget-shadow{margin:-5px 0 0 -5px;padding:5px;background:#000 url("images/ui-bg_flat_10_000000_40x100.png") 50% 50% repeat-x;opacity:.2;filter:Alpha(Opacity=20);border-radius:5px} -------------------------------------------------------------------------------- /public/static/css/sidebar-bootstrap.css: -------------------------------------------------------------------------------- 1 | .navbar-fixed-top + .sidebar-trigger .sidebar-toggle, 2 | .navbar-fixed-bottom + .sidebar-trigger .sidebar-toggle, 3 | .navbar-fixed-top + .sidebar-trigger + .sidebar-trigger .sidebar-toggle, 4 | .navbar-fixed-bottom + .sidebar-trigger + .sidebar-trigger .sidebar-toggle { 5 | position: fixed; 6 | z-index: 1032; 7 | } 8 | .navbar-fixed-top + .sidebar-trigger .sidebar-wrapper, 9 | .navbar-fixed-bottom + .sidebar-trigger .sidebar-wrapper, 10 | .navbar-fixed-top + .sidebar-trigger + .sidebar-trigger .sidebar-wrapper, 11 | .navbar-fixed-bottom + .sidebar-trigger + .sidebar-trigger .sidebar-wrapper { 12 | z-index: 1033; 13 | } 14 | .navbar-fixed-bottom + .sidebar-trigger .sidebar-toggle, 15 | .navbar-fixed-bottom + .sidebar-trigger + .sidebar-trigger .sidebar-toggle { 16 | top: inherit; 17 | bottom: 0; 18 | } 19 | .navbar-fixed-top + .container, 20 | .navbar-fixed-top + .container-fluid, 21 | .navbar-fixed-top + .sidebar-trigger + .container, 22 | .navbar-fixed-top + .sidebar-trigger + .container-fluid, 23 | .navbar-fixed-top + .sidebar-trigger + .sidebar-trigger + .container, 24 | .navbar-fixed-top + .sidebar-trigger + .sidebar-trigger + .container-fluid { 25 | margin-top: 70px; 26 | } 27 | .navbar-fixed-bottom + .container, 28 | .navbar-fixed-bottom + .container-fluid, 29 | .navbar-fixed-bottom + .sidebar-trigger + .container, 30 | .navbar-fixed-bottom + .sidebar-trigger + .container-fluid, 31 | .navbar-fixed-bottom + .sidebar-trigger + .sidebar-trigger + .container, 32 | .navbar-fixed-bottom + .sidebar-trigger + .sidebar-trigger + .container-fluid { 33 | margin-bottom: 70px; 34 | } 35 | @media (min-width: 992px) { 36 | .sidebar-force-open:not(.sidebar-right) + .container, 37 | .sidebar-force-open:not(.sidebar-right) + .sidebar-trigger + .container { 38 | padding-left: 225px; 39 | } 40 | .sidebar-force-open:not(.sidebar-right) + .container-fluid, 41 | .sidebar-force-open:not(.sidebar-right) + .sidebar-trigger + .container-fluid { 42 | margin-left: 210px; 43 | } 44 | .sidebar-force-open.sidebar-right + .container, 45 | .sidebar-force-open.sidebar-right + .sidebar-trigger + .container { 46 | padding-right: 225px; 47 | } 48 | .sidebar-force-open.sidebar-right + .container-fluid, 49 | .sidebar-force-open.sidebar-right + .sidebar-trigger + .container-fluid { 50 | margin-right: 210px; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/static/css/sidebar.css: -------------------------------------------------------------------------------- 1 | .sidebar-toggle { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | height: 51px; 6 | cursor: pointer; 7 | margin: 0; 8 | padding: 17px 20px 17px 0; 9 | } 10 | .sidebar-toggle .glyphicon{ 11 | color:white; 12 | } 13 | .sidebar-toggle > i { 14 | font-size: 18px; 15 | margin: 0 0 0 -5px; 16 | -webkit-transition: all 0.1s ease-in-out; 17 | transition: all 0.1s ease-in-out; 18 | } 19 | .sidebar-toggle > .fa-sidebar-toggle:before { 20 | content: "\f0c9"; 21 | } 22 | .sidebar-toggle:hover > i, 23 | .sidebar-toggle.sidebar-toggle-opened > i { 24 | margin-left: -9px; 25 | } 26 | .sidebar-toggle + .navbar-brand > img { 27 | margin-left: 10px; 28 | } 29 | .sidebar-togglable .sidebar-toggle { 30 | display: block; 31 | } 32 | .sidebar-wrapper { 33 | position: fixed; 34 | top: 0; 35 | overflow: hidden; 36 | bottom: 0; 37 | width: 210px; 38 | cursor: default; 39 | -moz-user-select: -moz-none; 40 | user-select: none; 41 | -webkit-transform: translate3d(-210px, 0px, 0px); 42 | transform: translate3d(-210px, 0px, 0px); 43 | } 44 | .sidebar-wrapper.sidebar-ready { 45 | -webkit-transition: -webkit-transform 0.2s; 46 | transition: transform 0.2s; 47 | } 48 | .sidebar-wrapper.sidebar-open { 49 | -webkit-transform: translate3d(0px, 0px, 0px); 50 | transform: translate3d(0px, 0px, 0px); 51 | } 52 | .sidebar-wrapper .sidebar-scroller { 53 | position: absolute; 54 | overflow-x: hidden; 55 | overflow-y: scroll; 56 | left: 0; 57 | right: -18px; 58 | height: 100%; 59 | } 60 | .sidebar-wrapper .sidebar-menu { 61 | margin: 0; 62 | padding: 0; 63 | overflow-x: hidden; 64 | list-style: none; 65 | text-align: left; 66 | font-size: 14px; 67 | } 68 | .sidebar-wrapper .sidebar-menu ul { 69 | padding: 0; 70 | margin: 0; 71 | } 72 | .sidebar-wrapper .sidebar-menu li { 73 | display: block; 74 | } 75 | .sidebar-wrapper .sidebar-group > span, 76 | .sidebar-wrapper .sidebar-item > a { 77 | display: block; 78 | height: 100%; 79 | padding: 12px 15px; 80 | text-decoration: none; 81 | } 82 | .sidebar-wrapper .sidebar-item > a.active { 83 | border-left: 5px solid; 84 | padding: 12px 15px 12px 10px; 85 | } 86 | .sidebar-wrapper .sidebar-item.sidebar-item-mini { 87 | font-size: 0.7em; 88 | } 89 | .sidebar-wrapper .sidebar-item.sidebar-item-mini > a { 90 | padding-top: 7px; 91 | padding-bottom: 7px; 92 | } 93 | .sidebar-wrapper .sidebar-group { 94 | margin-top: 20px; 95 | } 96 | .sidebar-wrapper .sidebar-group:first-child { 97 | margin-top: 0; 98 | } 99 | .sidebar-wrapper .sidebar-group > span { 100 | font-family: inherit; 101 | font-size: 24px; 102 | border-bottom: 1px solid transparent; 103 | } 104 | .sidebar-wrapper .sidebar-group.sticky-header { 105 | position: absolute; 106 | height: auto; 107 | top: 0; 108 | left: 0; 109 | right: 0; 110 | z-index: 1; 111 | margin-top: 0; 112 | } 113 | .sidebar-wrapper .sidebar-group + .sidebar-item { 114 | margin-top: 32px; 115 | border-top: 1px solid transparent; 116 | } 117 | .sidebar-wrapper .sidebar-item > a { 118 | cursor: pointer; 119 | } 120 | .sidebar-swipe { 121 | position: fixed; 122 | z-index: 1001; 123 | width: 20px; 124 | left: 0; 125 | top: 0; 126 | bottom: 0; 127 | -moz-user-select: -moz-none; 128 | user-select: none; 129 | } 130 | .sidebar-open + .sidebar-swipe, 131 | .sidebar-force-open .sidebar-swipe { 132 | left: 210px; 133 | } 134 | .sidebar-trigger .sidebar-toggle { 135 | z-index: 1002; 136 | } 137 | .sidebar-trigger .sidebar-wrapper { 138 | z-index: 1003; 139 | } 140 | .sidebar-wrapper.sidebar-default { 141 | color: #222222; 142 | background-color: #ffffff; 143 | } 144 | .sidebar-wrapper.sidebar-default.sidebar-open, 145 | .sidebar-wrapper.sidebar-default.sidebar-dragging { 146 | -webkit-box-shadow: 3px 0 4px rgba(0, 0, 0, 0.18); 147 | box-shadow: 3px 0 4px rgba(0, 0, 0, 0.18); 148 | } 149 | .sidebar-wrapper.sidebar-default .sidebar-group > span { 150 | color: #dbdbdb; 151 | border-bottom-color: #eeeeee; 152 | } 153 | .sidebar-wrapper.sidebar-default .sidebar-group.sticky-header > span { 154 | background-color: #ffffff; 155 | } 156 | .sidebar-wrapper.sidebar-default .sidebar-group + .sidebar-item { 157 | border-top-color: #eeeeee; 158 | } 159 | .sidebar-wrapper.sidebar-default .sidebar-item > a { 160 | color: #222222; 161 | background-color: #ffffff; 162 | } 163 | .sidebar-wrapper.sidebar-default .sidebar-item > a:hover, 164 | .sidebar-wrapper.sidebar-default .sidebar-item > a:focus { 165 | color: #333333; 166 | background-color: #eeeeee; 167 | } 168 | .sidebar-wrapper.sidebar-default .sidebar-item > a.active { 169 | color: #337ab7; 170 | background-color: #ffffff; 171 | } 172 | .sidebar-wrapper.sidebar-default .sidebar-item.sidebar-item-mini > a { 173 | color: #747474; 174 | } 175 | .sidebar-wrapper.sidebar-default .hammer-scrollbar { 176 | background-color: #555555; 177 | } 178 | .sidebar-wrapper.sidebar-inverse { 179 | color: #9d9d9d; 180 | background-color: #2a3542; 181 | } 182 | .sidebar-wrapper.sidebar-inverse.sidebar-open, 183 | .sidebar-wrapper.sidebar-inverse.sidebar-dragging { 184 | -webkit-box-shadow: 3px 0 4px rgba(0, 0, 0, 0.32); 185 | box-shadow: 3px 0 4px rgba(0, 0, 0, 0.32); 186 | } 187 | .sidebar-wrapper.sidebar-inverse .sidebar-group > span { 188 | color: #46586e; 189 | border-bottom-color: #344252; 190 | } 191 | .sidebar-wrapper.sidebar-inverse .sidebar-group.sticky-header > span { 192 | background-color: #2a3542; 193 | } 194 | .sidebar-wrapper.sidebar-inverse .sidebar-group + .sidebar-item { 195 | border-top-color: #344252; 196 | } 197 | .sidebar-wrapper.sidebar-inverse .sidebar-item > a { 198 | color: #9d9d9d; 199 | background-color: #2a3542; 200 | } 201 | .sidebar-wrapper.sidebar-inverse .sidebar-item > a:hover, 202 | .sidebar-wrapper.sidebar-inverse .sidebar-item > a:focus { 203 | color: #ffffff; 204 | background-color: #344252; 205 | } 206 | .sidebar-wrapper.sidebar-inverse .sidebar-item > a.active { 207 | color: #dfecf6; 208 | background-color: #2a3542; 209 | } 210 | .sidebar-wrapper.sidebar-inverse .sidebar-item.sidebar-item-mini > a { 211 | color: #6d85a2; 212 | } 213 | .sidebar-wrapper.sidebar-inverse .hammer-scrollbar { 214 | background-color: #e4e8ed; 215 | } 216 | .navbar-default + .sidebar-trigger .sidebar-toggle > i, 217 | .navbar-default + .sidebar-trigger + .sidebar-trigger .sidebar-toggle > i { 218 | color: #888888; 219 | } 220 | .navbar-default + .sidebar-trigger .sidebar-toggle:hover > i, 221 | .navbar-default + .sidebar-trigger + .sidebar-trigger .sidebar-toggle:hover > i { 222 | color: #333333; 223 | } 224 | .navbar-inverse + .sidebar-trigger .sidebar-toggle > i, 225 | .navbar-inverse + .sidebar-trigger + .sidebar-trigger .sidebar-toggle > i { 226 | color: #ffffff; 227 | } 228 | .navbar-inverse + .sidebar-trigger .sidebar-toggle:hover > i, 229 | .navbar-inverse + .sidebar-trigger + .sidebar-trigger .sidebar-toggle:hover > i { 230 | color: #ffffff; 231 | } 232 | .sidebar-right .sidebar-toggle { 233 | left: auto; 234 | right: 0; 235 | padding-left: 20px; 236 | padding-right: 0; 237 | } 238 | .sidebar-right .sidebar-toggle > i { 239 | margin-left: 0; 240 | margin-right: -5px; 241 | } 242 | .sidebar-right .sidebar-toggle:hover > i, 243 | .sidebar-right .sidebar-toggle.sidebar-toggle-opened > i { 244 | margin-right: -9px; 245 | } 246 | .sidebar-right .sidebar-wrapper { 247 | right: 0; 248 | -webkit-transform: translate3d(210px, 0px, 0px); 249 | transform: translate3d(210px, 0px, 0px); 250 | } 251 | .sidebar-right .sidebar-wrapper.sidebar-open { 252 | -webkit-transform: translate3d(0px, 0px, 0px); 253 | transform: translate3d(0px, 0px, 0px); 254 | } 255 | .sidebar-right .sidebar-wrapper.sidebar-default.sidebar-open, 256 | .sidebar-right .sidebar-wrapper.sidebar-default.sidebar-dragging { 257 | -webkit-box-shadow: -3px 0 4px rgba(0, 0, 0, 0.18); 258 | box-shadow: -3px 0 4px rgba(0, 0, 0, 0.18); 259 | } 260 | .sidebar-right .sidebar-wrapper.sidebar-inverse.sidebar-open, 261 | .sidebar-right .sidebar-wrapper.sidebar-inverse.sidebar-dragging { 262 | -webkit-box-shadow: -3px 0 4px rgba(0, 0, 0, 0.32); 263 | box-shadow: -3px 0 4px rgba(0, 0, 0, 0.32); 264 | } 265 | .sidebar-right .sidebar-wrapper .sidebar-item > a.active { 266 | border-left: inherit; 267 | border-right: 5px solid; 268 | padding: 12px 10px 12px 15px; 269 | } 270 | .sidebar-right .sidebar-swipe { 271 | left: auto; 272 | right: 0; 273 | } 274 | .sidebar-right .sidebar-open + .sidebar-swipe, 275 | .sidebar-right .sidebar-force-open .sidebar-swipe { 276 | right: 210px; 277 | } 278 | @media (max-width: 767px) { 279 | .sidebar-wrapper { 280 | width: 80%; 281 | -webkit-transform: translate3d(-100%, 0px, 0px); 282 | transform: translate3d(-100%, 0px, 0px); 283 | } 284 | .sidebar-open + .sidebar-swipe, 285 | .sidebar-force-open .sidebar-open + .sidebar-swipe { 286 | left: 80%; 287 | } 288 | .sidebar-force-open .sidebar-swipe { 289 | left: 0; 290 | } 291 | .sidebar-right .sidebar-toggle { 292 | padding-left: 7px; 293 | } 294 | .sidebar-right .sidebar-wrapper { 295 | -webkit-transform: translate3d(100%, 0px, 0px); 296 | transform: translate3d(100%, 0px, 0px); 297 | } 298 | .sidebar-right .sidebar-open + .sidebar-swipe { 299 | left: auto; 300 | right: 80%; 301 | } 302 | .sidebar-right .sidebar-force-open .sidebar-swipe { 303 | left: auto; 304 | right: 0; 305 | } 306 | } 307 | @media (max-width: 991px) { 308 | .sidebar-force-open .sidebar-wrapper:not(.sidebar-open) + .sidebar-swipe { 309 | left: 0; 310 | } 311 | } 312 | @media (min-width: 992px) { 313 | .sidebar-trigger.sidebar-locked .sidebar-toggle { 314 | display: none; 315 | } 316 | .sidebar-trigger.sidebar-locked .sidebar-wrapper { 317 | margin-top: 51px; 318 | } 319 | .sidebar-wrapper.sidebar-open-init { 320 | -webkit-transform: translate3d(0px, 0px, 0px); 321 | transform: translate3d(0px, 0px, 0px); 322 | } 323 | .sidebar-force-open .sidebar-wrapper.sidebar-open, 324 | .sidebar-force-open.sidebar-right .sidebar-wrapper.sidebar-open, 325 | .sidebar-force-open .sidebar-wrapper.sidebar-dragging, 326 | .sidebar-force-open.sidebar-right .sidebar-wrapper.sidebar-dragging { 327 | -webkit-box-shadow: none; 328 | box-shadow: none; 329 | } 330 | } 331 | .sidebar-wrapper { 332 | -ms-touch-action: none; 333 | } 334 | .sidebar-swipe { 335 | -ms-touch-action: none; 336 | } 337 | -------------------------------------------------------------------------------- /public/static/css/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | omninotesweb main stylesheet 4 | ============= 5 | 6 | Author: Suraj Patil 7 | Updated: January 13, 2014 8 | Notes: omninotesweb 9 | 10 | */ 11 | 12 | 13 | /*-------------------------------------- 14 | Layout 15 | -------------------------------------- */ 16 | 17 | .commentslist { 18 | background-color: #f5f5f5; 19 | } 20 | 21 | .loginbutton { 22 | height: 38px; 23 | padding: 8px 12px; 24 | font-size: 14px; 25 | line-height: 1.428571429; 26 | color: #555; 27 | background-color: #fff; 28 | border: 1px solid #ccc; 29 | border-radius: 4px; 30 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 31 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 32 | -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; 33 | transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s 34 | } 35 | 36 | .comment { 37 | padding-left: 25px; 38 | padding-right: 25px; 39 | padding-top: 15px; 40 | } 41 | 42 | ul { 43 | list-style-type: none; 44 | } 45 | 46 | .timestamp { 47 | color: #aaa; 48 | } 49 | 50 | .badge { 51 | background-color: #1a78c9; 52 | margin-right: 10px; 53 | float: right; 54 | } 55 | 56 | .center { 57 | text-align: center; 58 | } 59 | 60 | .navbar-brand:hover { 61 | color: white; 62 | } 63 | 64 | .footer { 65 | height: 30px; 66 | width: auto; 67 | text-align: center; 68 | } 69 | 70 | .floating-action-icon { 71 | position: fixed; 72 | bottom: 40px; 73 | right: 30px; 74 | z-index: 101; 75 | width: 56px; 76 | height: 56px; 77 | font-size: 20px; 78 | border-radius: 35px; 79 | box-shadow: 0 0 4px #111; 80 | padding-top: 10px !important; 81 | } 82 | 83 | .notification { 84 | width: -moz-fit-content; 85 | position: fixed; 86 | right: 10px; 87 | max-width: 400px; 88 | padding: 12px; 89 | background-color: #F2FCFF; 90 | box-shadow: 1px 0px 9px 0px rgba(27, 123, 216, 0.4); 91 | border: 1px solid #5596CE; 92 | border-radius: 10px; 93 | border-radius: 10px; 94 | z-index: 1100; 95 | max-height: 90px; 96 | margin-top: 20px; 97 | min-width: 200px; 98 | } 99 | 100 | #btnMessage { 101 | padding: 1px 12px; 102 | height: 30px; 103 | text-align: center; 104 | float: right; 105 | } 106 | 107 | 108 | /*-------------------------------------- 109 | Navbar 110 | -------------------------------------- */ 111 | 112 | .mainHeader { 113 | background-color: #3f51b5; 114 | border-radius: 0px; 115 | } 116 | 117 | .navbar-brand { 118 | margin-left: 25px; 119 | font-size: 1.5em; 120 | color: white; 121 | max-width: 2000px; 122 | } 123 | 124 | .btn-action { 125 | background-color: #3f51b5; 126 | } 127 | 128 | #icons { 129 | text-align: right; 130 | float: right; 131 | height: 50%; 132 | line-height: 52px; 133 | float: right; 134 | margin-top: 10px; 135 | } 136 | 137 | nav .glyphicon { 138 | color: white; 139 | } 140 | 141 | nav .glyphicon:hover { 142 | color: black; 143 | } 144 | 145 | .sr-only { 146 | display: block; 147 | width: 20px; 148 | height: 3px; 149 | background-color: white; 150 | margin-bottom: 4px; 151 | } 152 | 153 | .hidden { 154 | display: none; 155 | } 156 | 157 | 158 | /*-------------------------------------- 159 | NotesFeed 160 | -------------------------------------- */ 161 | 162 | .overdue { 163 | color: red; 164 | margin-top: 1px; 165 | font-size: 12px; 166 | padding-top: -100px; 167 | position: absolute; 168 | padding-left: 5px; 169 | } 170 | 171 | .noteHeading { 172 | font-weight: 900; 173 | font-size: 17px; 174 | color: #666666; 175 | margin-bottom: 0px; 176 | padding-bottom: 5px; 177 | } 178 | 179 | hr { 180 | margin: 0; 181 | padding: 0; 182 | } 183 | 184 | .noteHeading a { 185 | font-weight: 400; 186 | font-size: 11px; 187 | } 188 | 189 | .noteContent { 190 | padding-top: 7px; 191 | font-size: 0.9em; 192 | } 193 | 194 | .noteContent img { 195 | width: 90%; 196 | padding-left: 10%; 197 | } 198 | 199 | .toggle { 200 | cursor: pointer; 201 | margin-top: -30px; 202 | float: right; 203 | } 204 | 205 | .notefooter { 206 | color: #aaa; 207 | height: 20px; 208 | } 209 | 210 | 211 | /* These are the classes that are going to be applied: */ 212 | 213 | .column { 214 | float: left; 215 | } 216 | 217 | .note { 218 | display: block; 219 | margin: 4px; 220 | margin-bottom: 6px; 221 | padding: 15px; 222 | background-color: white; 223 | box-shadow: 1px 1px 2px 1px #7D7470; 224 | border-radius: 4px 4px 4px 4px; 225 | height: auto; 226 | } 227 | 228 | .note:hover { 229 | border-bottom: 2px solid black; 230 | } 231 | 232 | .menu a { 233 | text-decoration: none; 234 | } 235 | 236 | .menu li { 237 | margin-top: -10px; 238 | list-style: none; 239 | float: right; 240 | padding: 5px; 241 | height: 15px; 242 | } 243 | 244 | 245 | /*-------------------------------------- 246 | Navigation drawer 247 | -------------------------------------- */ 248 | 249 | .list-group-item { 250 | border: none; 251 | } 252 | 253 | .sidebar-item h5 { 254 | margin-left: 15px; 255 | display: block; 256 | } 257 | 258 | .sidebar-item .glyphicon:hover { 259 | color: black; 260 | } 261 | 262 | .col-sm-2 a.list-group-item:hover, 263 | a.list-group-item:focus { 264 | text-decoration: none; 265 | background-color: transparent; 266 | } 267 | 268 | body { 269 | background-color: #f5f5f5; 270 | overflow-x: hidden; 271 | overflow-y: auto; 272 | margin-top: 70px; 273 | } 274 | 275 | .nav-item { 276 | padding-left: 10px; 277 | } 278 | 279 | .col-sm-2 { 280 | background-color: white; 281 | } 282 | 283 | .badge { 284 | background-color: #7D8EF0; 285 | margin-right: 10px; 286 | float: right; 287 | } 288 | 289 | 290 | /* -------------------------------------- 291 | Global Styles 292 | -------------------------------------- */ 293 | 294 | body { 295 | width: 100%; 296 | } 297 | 298 | .main-content { 299 | color: black; 300 | } 301 | 302 | .highlight { 303 | background-color: #72CBFF; 304 | } 305 | 306 | 307 | /* -------------------------------------- 308 | Media Queries 309 | -------------------------------------- */ 310 | 311 | 312 | /* Portrait & landscape phone */ 313 | 314 | @media (max-width: 480px) { 315 | body { 316 | margin-top: 105px; 317 | } 318 | 319 | textarea { 320 | resize: vertical; 321 | width:50%; 322 | } 323 | 324 | .timeline { 325 | overflow: hidden; 326 | width: 100%; 327 | } 328 | 329 | #icons { 330 | text-align: center; 331 | padding: 0; 332 | height: 50%; 333 | line-height: 52px; 334 | float: right; 335 | margin-top: 10px; 336 | } 337 | .mainHeader h3 { 338 | padding: 0px; 339 | text-align: center; 340 | } 341 | 342 | .btn-primary { 343 | width: auto; 344 | } 345 | 346 | .input { 347 | width:100px; 348 | } 349 | 350 | .floating-action-icon { 351 | position: fixed; 352 | bottom: 35px; 353 | right: 20px; 354 | z-index: 101; 355 | width: 50px; 356 | height: 50px; 357 | padding: 10px 10px; 358 | font-size: 24px; 359 | border-radius: 25px; 360 | text-align: center; 361 | } 362 | .modal-dialog { 363 | width: 240px; 364 | } 365 | } 366 | 367 | 368 | /* Landscape phone to portrait tablet */ 369 | 370 | @media (max-width: 768px) and (min-width:481px) { 371 | #icons { 372 | text-align: center; 373 | padding: 0; 374 | height: 50%; 375 | line-height: 52px; 376 | float: right; 377 | margin-top: 10px; 378 | } 379 | 380 | textarea { 381 | resize: vertical; 382 | width: 200px; 383 | } 384 | 385 | body { 386 | margin-top: 105px; 387 | } 388 | 389 | .timeline { 390 | overflow: hidden; 391 | width: 80%; 392 | padding-left: 40px; 393 | } 394 | .mainHeader h3 { 395 | padding: 0px; 396 | text-align: center; 397 | } 398 | .note-lg { 399 | height: 360px; 400 | /*change later*/ 401 | } 402 | .modal-dialog { 403 | width: 250px; 404 | } 405 | } 406 | 407 | 408 | /* Large desktop */ 409 | 410 | @media (max-width: 1400px) and (min-width:769px) { 411 | .timeline { 412 | overflow: hidden; 413 | width: 55%; 414 | margin-left: 20%; 415 | } 416 | 417 | textarea { 418 | resize: vertical; 419 | width: 400px; 420 | } 421 | } 422 | 423 | 424 | /*-------------------------------------- 425 | Sidebar 426 | -------------------------------------- */ 427 | 428 | .sidebar-toggle { 429 | left: 10px !important; 430 | } 431 | 432 | .sidebar-wrapper.sidebar-default.sidebar-open, 433 | .sidebar-wrapper.sidebar-default.sidebar-dragging { 434 | width: 260px; 435 | } 436 | 437 | 438 | /*Modal dialog*/ 439 | 440 | .modal-footer { 441 | padding: 10px 15px 15px; 442 | margin: 0; 443 | border: none; 444 | } 445 | 446 | .modal-body { 447 | padding: 20px 20px 10px 20px; 448 | } 449 | 450 | .modal { 451 | overflow: hidden; 452 | } 453 | -------------------------------------------------------------------------------- /public/static/fonts/glyphiconshalflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thewhitetulip/Tasks-vue/d9da739f0ff5968abdac9555e1321dcf30b931c5/public/static/fonts/glyphiconshalflings-regular.eot -------------------------------------------------------------------------------- /public/static/fonts/glyphiconshalflings-regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thewhitetulip/Tasks-vue/d9da739f0ff5968abdac9555e1321dcf30b931c5/public/static/fonts/glyphiconshalflings-regular.otf -------------------------------------------------------------------------------- /public/static/fonts/glyphiconshalflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thewhitetulip/Tasks-vue/d9da739f0ff5968abdac9555e1321dcf30b931c5/public/static/fonts/glyphiconshalflings-regular.ttf -------------------------------------------------------------------------------- /public/static/fonts/glyphiconshalflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thewhitetulip/Tasks-vue/d9da739f0ff5968abdac9555e1321dcf30b931c5/public/static/fonts/glyphiconshalflings-regular.woff -------------------------------------------------------------------------------- /public/static/js/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | Author: Suraj Patil http://github.com/thewhitetulip 3 | License: MIT 4 | 5 | This is the Vue front end for the Tasks application. We will not be using webpack or any other fancy tech. Plain JS. 6 | You are expected to know a little bit of JS. If you are a total newbie, we recommend reading, 7 | https://github.com/getify/You-Dont-Know-JS 8 | You certainly do not need to be a pro in JS, just need to know enough to follow along, but you do need to read the book 9 | eventually some day to become comfortable with the concepts in JS, so we recommend strongly to read the book before continuing. 10 | */ 11 | 12 | var delimiters = ["${", "}"]; 13 | 14 | // add-comment is the component for adding a comment. 15 | Vue.component('add-comment', { 16 | props:["taskindex", "comment"], 17 | delimiters: delimiters, 18 | data: function() { 19 | return app.comment 20 | }, 21 | template: '

\ 22 |
\ 23 | \ 24 | \ 25 |
', 26 | methods: { 27 | addComment: function (comment, taskIndex) { 28 | app.comment.taskID = app.tasks[taskIndex].id; 29 | console.log(app.tasks[taskIndex].title, app.tasks[taskIndex].id); 30 | 31 | if (app.comment.content == '') { 32 | app.notify("Comment can't be empty"); 33 | return; 34 | } 35 | 36 | this.$http.put('/comment/', app.comment, { 37 | emulateJSON: true 38 | }).then(response => response.json()).then(result => { 39 | app.comment.author = result.author; 40 | app.comment.created = result.created; 41 | 42 | if (app.tasks[taskIndex].comments == null) { 43 | app.tasks[taskIndex].comments = []; 44 | } 45 | 46 | app.tasks[taskIndex].comments.push(comment); 47 | app.comment = { 48 | content: '', 49 | created: '', 50 | taskID: '', 51 | author: '' 52 | } 53 | 54 | app.notify("Comment added") 55 | 56 | }).catch(err => { 57 | console.log(err); 58 | this.notify("Unable to add comment"); 59 | }); 60 | } 61 | } 62 | }); 63 | 64 | // comment is the component written for the a task comment, yet to include the input tag for adding comment 65 | Vue.component('comment', { 66 | delimiters: delimiters, 67 | template:'
\ 68 |

\ 69 | ${comment.author} ${comment.created}\ 70 | \ 71 |
', 72 | props:["comment","cindex", "taskid", "taskindex"], 73 | methods:{ 74 | deleteComment: function (taskIndex, commentIndex, taskID, commentID) { 75 | this.$http.delete('/comment/' + commentID).then(response => response.json()) 76 | .then(result => { 77 | app.tasks[taskIndex].comments.splice(commentIndex, 1); 78 | app.notify("Comment deleted"); 79 | }).catch(err => { 80 | console.log(err); 81 | app.notify("Unable to delete comment"); 82 | }); 83 | } 84 | } 85 | }); 86 | 87 | var app = new Vue({ 88 | // The element in the html page where Vue will be anchored 89 | el: '#tasks', 90 | // The delimiters used in our app, standard delimiters modified as Go uses {{. 91 | delimiters: delimiters, 92 | data: { 93 | categories: [], 94 | tasks: [], 95 | navigation: 'Pending', // Displays in the status bar (Completed/Deleted/Pending) 96 | isLoggedIn: false, 97 | user: '', 98 | newCategoryName: '', // The new category name to be used in the update category flow 99 | notificationVisible: false, // Toggles the visibility of the notification 100 | notification: '', // Actual content of the notification. 101 | isEditing: false, // Decides if we are editing or adding a task, updated whenever we click the edit function, set to true 102 | taskIDEdit: -1, // Stores the task id which is currently being updated; 103 | categoryEdit: false, // Toggles the display of the update category form 104 | userLogin: { // For the login form 105 | username: '', 106 | password: '' 107 | }, 108 | userSignup: { // For the signup form 109 | username: '', 110 | password: '', 111 | email: '' 112 | }, 113 | task: { // For add task form 114 | id: '', 115 | title: '', 116 | content: '', 117 | category: '', 118 | priority: '', 119 | comments: [], 120 | ishidden: '', 121 | completedmsg: '', 122 | showComment: false 123 | }, 124 | selectedCategoryName: '', // If the user has selected the category in the navigation drawer, this var has value. 125 | selectedTaskTypeName: 'pending', // If the user is at pending/deleted/completed this var has value. 126 | comment: { // For the add comment form. 127 | id: '', 128 | taskID: '', 129 | content: '', 130 | author: '', 131 | created: '' 132 | }, 133 | category: { // For add category form. 134 | categoryID: '', 135 | categoryName: '', 136 | taskCount: '' 137 | }, 138 | }, 139 | // mounted is called the moment the Vue app is added to the html DOM. 140 | // when the app is mounted, we check if the user is logged in or not 141 | // and update the isLoggedIn variable to true if yes, false if not. 142 | mounted: function () { 143 | this.checklogin(); 144 | }, 145 | methods: { 146 | // notify toggles the notification and set the content of the 147 | // notification with the argument passed in it. 148 | notify: function (message) { 149 | this.notificationVisible = true; 150 | this.notification = message; 151 | }, 152 | signup: function () { 153 | this.$http.put('/signup/', this.userSignup, { 154 | emulateJSON: true 155 | }).then(response => response.json()).then(result => { 156 | this.notify("Sign up successful, pls login"); 157 | this.userSignup = { 158 | username: '', 159 | password: '', 160 | email: '' 161 | } 162 | }).catch(err => { 163 | console.log(err); 164 | this.notify("Unable to signup"); 165 | }); 166 | }, 167 | // checklogin checks if the user is logged in, if the user is logged in, 168 | // fetches tasks and categories. 169 | checklogin: function () { 170 | this.$http.get('/login/').then(response => response.json()).then(result => { 171 | this.isLoggedIn = result.loggedin; 172 | if (this.isLoggedIn) { 173 | this.FetchCategories(); 174 | this.FetchTasks(); 175 | } 176 | }).catch(err => { 177 | console.log(err); 178 | this.notify("Something went wrong") 179 | }); 180 | }, 181 | login: function () { 182 | this.$http.post('/login/', this.userLogin, { 183 | emulateJSON: true 184 | }).then(response => response.json()).then(result => { 185 | this.isLoggedIn = true; 186 | this.FetchCategories(); 187 | this.FetchTasks(); 188 | this.userLogin = { 189 | username: '', 190 | password: '' 191 | } 192 | }).catch(err => { 193 | console.log(err); 194 | this.notify("Unable to login"); 195 | }); 196 | }, 197 | logout: function () { 198 | this.$http.get('/logout/').then(response => response.json()) 199 | .then(result => { 200 | this.isLoggedIn = false; 201 | }).catch(err => { 202 | console.log(err); 203 | this.notify("Unable to logout"); 204 | }); 205 | }, 206 | FetchTasks: function () { 207 | this.$http.get('/task/').then(response => response.json()).then(result => { 208 | if (result != null) { 209 | Vue.set(this.$data, 'tasks', result); 210 | } else { 211 | this.tasks = []; 212 | } 213 | }).catch(err => { 214 | console.log(err); 215 | isLoggedIn = false; 216 | this.notify("Unable to fetch Tasks") 217 | }); 218 | }, 219 | FetchCategories: function () { 220 | this.$http.get('/categories/').then(response => response.json()).then(result => { 221 | Vue.set(this.$data, 'categories', result); 222 | }).catch(err => { 223 | console.log(err); 224 | this.notify("Unable to fetch categories"); 225 | }); 226 | }, 227 | AddTask: function (item) { 228 | this.$http.put('/task/', this.task, { 229 | emulateJSON: true 230 | }).then(response => response).then(result => { 231 | if (this.task.ishidden == false) { 232 | this.tasks.push(this.task); 233 | } 234 | 235 | this.UpdateCategoryCount(this.task.category, "+", 1); 236 | 237 | this.task = { 238 | title: '', 239 | content: '', 240 | category: '', 241 | priority: '', 242 | comments: [], 243 | showComment: false 244 | } 245 | }).catch(err => { 246 | console.log(err); 247 | this.notify("Unable to add Task"); 248 | }); 249 | $('#addNoteModal').modal('hide'); 250 | }, 251 | toggleEditCategoryForm: function () { 252 | this.categoryEdit = !this.categoryEdit; 253 | }, 254 | // UpdateCategoryCount updates the count of the category where 255 | // some action took place, like add task or delete task. 256 | // our taskCount stores the # of tasks which are pending. 257 | // arguments: (category name, action(increase/decrease),value to increase or decrease), 258 | UpdateCategoryCount: function (name, action, value) { 259 | categoryIndex = 0; 260 | for (c in this.categories) { 261 | if (this.categories[c].categoryName == name) { 262 | switch (action) { 263 | case "+": 264 | this.categories[c].taskCount += value; 265 | case "-": 266 | this.categories[c].taskCount -= value; 267 | } 268 | break; 269 | } 270 | } 271 | }, 272 | UpdateTask: function (item) { 273 | this.$http.post('/task/', this.task, { 274 | emulateJSON: true 275 | }).then(response => response).then(result => { 276 | index = 0; 277 | for (t in this.tasks) { 278 | if (t.id == this.taskIDEdit) { 279 | index = this.tasks.indexOf(t); 280 | } 281 | } 282 | newTask = this.task; 283 | 284 | this.tasks[index].title = newTask.title; 285 | this.tasks[index].category = newTask.category; 286 | this.tasks[index].content = newTask.content; 287 | this.tasks[index].priority = newTask.priority; 288 | 289 | this.notify("Updated task"); 290 | 291 | this.task = { 292 | title: '', 293 | content: '', 294 | category: '', 295 | priority: '', 296 | comments: [], 297 | showComment: false 298 | } 299 | }).catch(err => { 300 | console.log(err); 301 | this.notify("Unable to update Task"); 302 | }); 303 | $('#addNoteModal').modal('hide'); 304 | }, 305 | addCategory: function () { 306 | console.log(this.category); 307 | this.$http.put("/category/", this.category, { 308 | emulateJSON: true 309 | }).then(response => response.json()).then(result => { 310 | this.category.taskCount = 0; 311 | this.categories.push(this.category); 312 | this.category = { 313 | categoryID: '', 314 | categoryName: '', 315 | taskCount: '' 316 | }; 317 | this.notify('Category Added'); 318 | }).catch(err => { 319 | console.log(err); 320 | this.notify("Unable to add category"); 321 | }); 322 | }, 323 | deleteCategory: function (name) { 324 | this.$http.delete('/category/' + name).then(response => response.json()) 325 | .then(result => { 326 | console.log('deleting ' + name); 327 | var index = 0; 328 | for (category in this.categories) { 329 | if (this.categories[category].categoryName == name) { 330 | index = this.categories.indexOf(category); 331 | } 332 | } 333 | this.categories.splice(index, 1); 334 | this.FetchTasks(); 335 | this.navigation = 'Pending'; 336 | this.selectedTaskTypeName = 'pending' 337 | }).catch(err => { 338 | console.log(err); 339 | this.notify("Unable to delete category"); 340 | }); 341 | }, 342 | // hides the visibility of the notification 343 | hide: function () { 344 | this.notificationVisible = false; 345 | }, 346 | // edit loads a task to the edit form. User can click on the button to update 347 | // the task, it calls UpdateTask function. 348 | edit: function (index) { 349 | this.isEditing = true; 350 | t = this.tasks[index]; 351 | this.task.title = t.title; 352 | this.task.id = t.id; 353 | this.taskIDEdit = t.id; 354 | this.task.content = t.content; 355 | this.task.priority = t.priority; 356 | this.task.category = t.category; 357 | $('#addNoteModal').modal('show'); 358 | }, 359 | // Trashs a task, won't delete from db. 360 | TrashTask: function (taskIndex, taskID, category) { 361 | this.$http.delete('/task/' + taskID).then(response => response.json()).then(result => { 362 | this.tasks.splice(taskIndex, 1); 363 | this.notify("Task deleted"); 364 | this.UpdateCategoryCount(category, "-", 1); 365 | }).catch(err => { 366 | console.log(err); 367 | this.notify("Unable to trash Tash"); 368 | }); 369 | }, 370 | RestoreTask: function (taskIndex, taskID, category) { 371 | this.$http.get('/restore-task/' + taskID).then(response => response.json()).then(result => { 372 | this.tasks.splice(index, 1); 373 | this.notify("Task restored"); 374 | this.UpdateCategoryCount(category, "+", 1); 375 | }).catch(err => { 376 | console.log(err); 377 | this.notify("Unable to restore task"); 378 | 379 | }); 380 | }, 381 | CompleteTask: function (taskIndex, taskID, category) { 382 | this.$http.get('/complete-task/' + taskID).then(response => response.json()).then(result => { 383 | this.tasks.splice(taskIndex, 1); 384 | this.notify("Marked task as complete"); 385 | this.UpdateCategoryCount(category, "-", 1); 386 | }).catch(err => { 387 | console.log(err); 388 | this.notify("Unable to mark as complete"); 389 | }); 390 | }, 391 | inComplete: function (taskIndex, taskID, category) { 392 | this.$http.get('/incomplete-task/' + taskID).then(response => response.json()).then(result => { 393 | this.tasks.splice(taskIndex, 1); 394 | this.notify("Marked task as incomplete"); 395 | this.UpdateCategoryCount(category, "+", 1); 396 | }).catch(err => { 397 | this.notify("Unable to mark task as incomplete"); 398 | }); 399 | }, 400 | // toggles the state to check which part is currently active 401 | // either pending/complete/deleted or categories 402 | taskByCategory: function (category) { 403 | this.selectedCategoryName = category; 404 | this.navigation = this.selectedCategoryName; 405 | this.tasks = []; 406 | this.tasks = []; 407 | this.selectedTaskTypeName = ''; 408 | this.$http.get('/category/' + this.selectedCategoryName).then(response => response.json()).then(result => { 409 | if (result != null) { 410 | Vue.set(this.$data, 'tasks', result); 411 | } 412 | }).catch(err => { 413 | console.log(err); 414 | this.notify("Unable to fetch tasks"); 415 | }); 416 | }, 417 | showCompletedTasks: function (type) { 418 | this.$http.get('/completed/').then(response => response.json()).then(result => { 419 | if (result != null) { 420 | Vue.set(this.$data, 'tasks', result); 421 | } 422 | this.selectedTaskTypeName = 'completed'; 423 | this.navigation = 'Completed'; 424 | this.selectedCategoryName = ''; 425 | }).catch(err => { 426 | console.log(err); 427 | this.notify("Unable to fetch tasks"); 428 | }); 429 | }, 430 | showPendingTasks: function (type) { 431 | this.FetchTasks(); 432 | this.selectedTaskTypeName = 'pending'; 433 | this.navigation = 'Pending'; 434 | this.selectedCategoryName = '' 435 | }, 436 | showDeletedTasks: function (type) { 437 | this.$http.get('/deleted/').then(response => response.json()).then(result => { 438 | if (result != null) { 439 | Vue.set(this.$data, 'tasks', result); 440 | } 441 | this.selectedTaskTypeName = 'deleted'; 442 | this.navigation = 'Deleted'; 443 | this.selectedCategoryName = '' 444 | }).catch(err => { 445 | console.log(err); 446 | this.notify("Unable to fetch tasks"); 447 | }); 448 | }, 449 | // Toggles the visibility of the note's comment area + content area 450 | toggleContent: function (item) { 451 | item.showComment = !item.showComment; 452 | }, 453 | updateCategory: function (oldName, newName) { 454 | category = { 455 | newCategoryName: this.newCategoryName 456 | } 457 | this.$http.post('/category/' + oldName, category, { 458 | emulateJSON: true 459 | }).then(response => response.json()).then(result => { 460 | 461 | for (category in this.categories) { 462 | if (this.categories[category].categoryName == oldName) { 463 | this.categories[category].categoryName = newName; 464 | console.log('Updated'); 465 | this.navigation = newName; 466 | this.toggleEditCategoryForm(); 467 | } 468 | } 469 | }).catch(err => { 470 | console.log(err); 471 | this.notify("Unable to update Task"); 472 | }); 473 | 474 | } 475 | } 476 | }) 477 | -------------------------------------------------------------------------------- /public/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * bootstrap.js v3.0.0 by @fat and @mdo 3 | * Copyright 2013 Twitter Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0 5 | */ 6 | if(!jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("webkitTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger("webkitTransitionEnd")};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(window.jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(window.jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]');if(a.length){var b=this.$element.find("input").prop("checked",!this.$element.hasClass("active"));"radio"===b.prop("type")&&a.find(".active").removeClass("active")}this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(window.jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover"},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;this.sliding=!0,f&&this.pause(),e=e.length?e:this.$element.find(".item")[h]();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(window.jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .accordion-group > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find("[data-toggle=collapse][data-parent="+i+"]").not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(window.jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title:empty").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip},b.prototype.destroy=function(){this.hide().$element.off("."+this.type).removeData(this.type)};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(window.jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(window.jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.attr("data-target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(window.jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(window.jQuery); -------------------------------------------------------------------------------- /public/static/js/hammer.min.js: -------------------------------------------------------------------------------- 1 | /*! Hammer.JS - v1.1.3 - 2014-05-20 2 | * http://eightmedia.github.io/hammer.js 3 | * 4 | * Copyright (c) 2014 Jorik Tangelder ; 5 | * Licensed under the MIT license */ 6 | 7 | 8 | !function(a,b){"use strict";function c(){d.READY||(s.determineEventTypes(),r.each(d.gestures,function(a){u.register(a)}),s.onTouch(d.DOCUMENT,n,u.detect),s.onTouch(d.DOCUMENT,o,u.detect),d.READY=!0)}var d=function v(a,b){return new v.Instance(a,b||{})};d.VERSION="1.1.3",d.defaults={behavior:{userSelect:"none",touchAction:"pan-y",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},d.DOCUMENT=document,d.HAS_POINTEREVENTS=navigator.pointerEnabled||navigator.msPointerEnabled,d.HAS_TOUCHEVENTS="ontouchstart"in a,d.IS_MOBILE=/mobile|tablet|ip(ad|hone|od)|android|silk/i.test(navigator.userAgent),d.NO_MOUSEEVENTS=d.HAS_TOUCHEVENTS&&d.IS_MOBILE||d.HAS_POINTEREVENTS,d.CALCULATE_INTERVAL=25;var e={},f=d.DIRECTION_DOWN="down",g=d.DIRECTION_LEFT="left",h=d.DIRECTION_UP="up",i=d.DIRECTION_RIGHT="right",j=d.POINTER_MOUSE="mouse",k=d.POINTER_TOUCH="touch",l=d.POINTER_PEN="pen",m=d.EVENT_START="start",n=d.EVENT_MOVE="move",o=d.EVENT_END="end",p=d.EVENT_RELEASE="release",q=d.EVENT_TOUCH="touch";d.READY=!1,d.plugins=d.plugins||{},d.gestures=d.gestures||{};var r=d.utils={extend:function(a,c,d){for(var e in c)!c.hasOwnProperty(e)||a[e]!==b&&d||(a[e]=c[e]);return a},on:function(a,b,c){a.addEventListener(b,c,!1)},off:function(a,b,c){a.removeEventListener(b,c,!1)},each:function(a,c,d){var e,f;if("forEach"in a)a.forEach(c,d);else if(a.length!==b){for(e=0,f=a.length;f>e;e++)if(c.call(d,a[e],e,a)===!1)return}else for(e in a)if(a.hasOwnProperty(e)&&c.call(d,a[e],e,a)===!1)return},inStr:function(a,b){return a.indexOf(b)>-1},inArray:function(a,b){if(a.indexOf){var c=a.indexOf(b);return-1===c?!1:c}for(var d=0,e=a.length;e>d;d++)if(a[d]===b)return d;return!1},toArray:function(a){return Array.prototype.slice.call(a,0)},hasParent:function(a,b){for(;a;){if(a==b)return!0;a=a.parentNode}return!1},getCenter:function(a){var b=[],c=[],d=[],e=[],f=Math.min,g=Math.max;return 1===a.length?{pageX:a[0].pageX,pageY:a[0].pageY,clientX:a[0].clientX,clientY:a[0].clientY}:(r.each(a,function(a){b.push(a.pageX),c.push(a.pageY),d.push(a.clientX),e.push(a.clientY)}),{pageX:(f.apply(Math,b)+g.apply(Math,b))/2,pageY:(f.apply(Math,c)+g.apply(Math,c))/2,clientX:(f.apply(Math,d)+g.apply(Math,d))/2,clientY:(f.apply(Math,e)+g.apply(Math,e))/2})},getVelocity:function(a,b,c){return{x:Math.abs(b/a)||0,y:Math.abs(c/a)||0}},getAngle:function(a,b){var c=b.clientX-a.clientX,d=b.clientY-a.clientY;return 180*Math.atan2(d,c)/Math.PI},getDirection:function(a,b){var c=Math.abs(a.clientX-b.clientX),d=Math.abs(a.clientY-b.clientY);return c>=d?a.clientX-b.clientX>0?g:i:a.clientY-b.clientY>0?h:f},getDistance:function(a,b){var c=b.clientX-a.clientX,d=b.clientY-a.clientY;return Math.sqrt(c*c+d*d)},getScale:function(a,b){return a.length>=2&&b.length>=2?this.getDistance(b[0],b[1])/this.getDistance(a[0],a[1]):1},getRotation:function(a,b){return a.length>=2&&b.length>=2?this.getAngle(b[1],b[0])-this.getAngle(a[1],a[0]):0},isVertical:function(a){return a==h||a==f},setPrefixedCss:function(a,b,c,d){var e=["","Webkit","Moz","O","ms"];b=r.toCamelCase(b);for(var f=0;f0&&this.started&&(g=n),this.started=!0;var j=this.collectEventData(c,g,e,a);return b!=o&&d.call(u,j),h&&(j.changedLength=i,j.eventType=h,d.call(u,j),j.eventType=g,delete j.changedLength),g==o&&(d.call(u,j),this.started=!1),g},determineEventTypes:function(){var b;return b=d.HAS_POINTEREVENTS?a.PointerEvent?["pointerdown","pointermove","pointerup pointercancel lostpointercapture"]:["MSPointerDown","MSPointerMove","MSPointerUp MSPointerCancel MSLostPointerCapture"]:d.NO_MOUSEEVENTS?["touchstart","touchmove","touchend touchcancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],e[m]=b[0],e[n]=b[1],e[o]=b[2],e},getTouchList:function(a,b){if(d.HAS_POINTEREVENTS)return t.getTouchList();if(a.touches){if(b==n)return a.touches;var c=[],e=[].concat(r.toArray(a.touches),r.toArray(a.changedTouches)),f=[];return r.each(e,function(a){r.inArray(c,a.identifier)===!1&&f.push(a),c.push(a.identifier)}),f}return a.identifier=1,[a]},collectEventData:function(a,b,c,d){var e=k;return r.inStr(d.type,"mouse")||t.matchType(j,d)?e=j:t.matchType(l,d)&&(e=l),{center:r.getCenter(c),timeStamp:Date.now(),target:d.target,touches:c,eventType:b,pointerType:e,srcEvent:d,preventDefault:function(){var a=this.srcEvent;a.preventManipulation&&a.preventManipulation(),a.preventDefault&&a.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return u.stopDetect()}}}},t=d.PointerEvent={pointers:{},getTouchList:function(){var a=[];return r.each(this.pointers,function(b){a.push(b)}),a},updatePointer:function(a,b){a==o||a!=o&&1!==b.buttons?delete this.pointers[b.pointerId]:(b.identifier=b.pointerId,this.pointers[b.pointerId]=b)},matchType:function(a,b){if(!b.pointerType)return!1;var c=b.pointerType,d={};return d[j]=c===(b.MSPOINTER_TYPE_MOUSE||j),d[k]=c===(b.MSPOINTER_TYPE_TOUCH||k),d[l]=c===(b.MSPOINTER_TYPE_PEN||l),d[a]},reset:function(){this.pointers={}}},u=d.detection={gestures:[],current:null,previous:null,stopped:!1,startDetect:function(a,b){this.current||(this.stopped=!1,this.current={inst:a,startEvent:r.extend({},b),lastEvent:!1,lastCalcEvent:!1,futureCalcEvent:!1,lastCalcData:{},name:""},this.detect(b))},detect:function(a){if(this.current&&!this.stopped){a=this.extendEventData(a);var b=this.current.inst,c=b.options;return r.each(this.gestures,function(d){!this.stopped&&b.enabled&&c[d.name]&&d.handler.call(d,a,b)},this),this.current&&(this.current.lastEvent=a),a.eventType==o&&this.stopDetect(),a}},stopDetect:function(){this.previous=r.extend({},this.current),this.current=null,this.stopped=!0},getCalculatedData:function(a,b,c,e,f){var g=this.current,h=!1,i=g.lastCalcEvent,j=g.lastCalcData;i&&a.timeStamp-i.timeStamp>d.CALCULATE_INTERVAL&&(b=i.center,c=a.timeStamp-i.timeStamp,e=a.center.clientX-i.center.clientX,f=a.center.clientY-i.center.clientY,h=!0),(a.eventType==q||a.eventType==p)&&(g.futureCalcEvent=a),(!g.lastCalcEvent||h)&&(j.velocity=r.getVelocity(c,e,f),j.angle=r.getAngle(b,a.center),j.direction=r.getDirection(b,a.center),g.lastCalcEvent=g.futureCalcEvent||a,g.futureCalcEvent=a),a.velocityX=j.velocity.x,a.velocityY=j.velocity.y,a.interimAngle=j.angle,a.interimDirection=j.direction},extendEventData:function(a){var b=this.current,c=b.startEvent,d=b.lastEvent||c;(a.eventType==q||a.eventType==p)&&(c.touches=[],r.each(a.touches,function(a){c.touches.push({clientX:a.clientX,clientY:a.clientY})}));var e=a.timeStamp-c.timeStamp,f=a.center.clientX-c.center.clientX,g=a.center.clientY-c.center.clientY;return this.getCalculatedData(a,d.center,e,f,g),r.extend(a,{startEvent:c,deltaTime:e,deltaX:f,deltaY:g,distance:r.getDistance(c.center,a.center),angle:r.getAngle(c.center,a.center),direction:r.getDirection(c.center,a.center),scale:r.getScale(c.touches,a.touches),rotation:r.getRotation(c.touches,a.touches)}),a},register:function(a){var c=a.defaults||{};return c[a.name]===b&&(c[a.name]=!0),r.extend(d.defaults,c,!0),a.index=a.index||1e3,this.gestures.push(a),this.gestures.sort(function(a,b){return a.indexb.index?1:0}),this.gestures}};d.Instance=function(a,b){var e=this;c(),this.element=a,this.enabled=!0,r.each(b,function(a,c){delete b[c],b[r.toCamelCase(c)]=a}),this.options=r.extend(r.extend({},d.defaults),b||{}),this.options.behavior&&r.toggleBehavior(this.element,this.options.behavior,!0),this.eventStartHandler=s.onTouch(a,m,function(a){e.enabled&&a.eventType==m?u.startDetect(e,a):a.eventType==q&&u.detect(a)}),this.eventHandlers=[]},d.Instance.prototype={on:function(a,b){var c=this;return s.on(c.element,a,b,function(a){c.eventHandlers.push({gesture:a,handler:b})}),c},off:function(a,b){var c=this;return s.off(c.element,a,b,function(a){var d=r.inArray({gesture:a,handler:b});d!==!1&&c.eventHandlers.splice(d,1)}),c},trigger:function(a,b){b||(b={});var c=d.DOCUMENT.createEvent("Event");c.initEvent(a,!0,!0),c.gesture=b;var e=this.element;return r.hasParent(b.target,e)&&(e=b.target),e.dispatchEvent(c),this},enable:function(a){return this.enabled=a,this},dispose:function(){var a,b;for(r.toggleBehavior(this.element,this.options.behavior,!1),a=-1;b=this.eventHandlers[++a];)r.off(this.element,b.gesture,b.handler);return this.eventHandlers=[],s.off(this.element,e[m],this.eventStartHandler),null}},function(a){function b(b,d){var e=u.current;if(!(d.options.dragMaxTouches>0&&b.touches.length>d.options.dragMaxTouches))switch(b.eventType){case m:c=!1;break;case n:if(b.distance0)){var k=Math.abs(d.options.dragMinDistance/b.distance);j.pageX+=b.deltaX*k,j.pageY+=b.deltaY*k,j.clientX+=b.deltaX*k,j.clientY+=b.deltaY*k,b=u.extendEventData(b)}(e.lastEvent.dragLockToAxis||d.options.dragLockToAxis&&d.options.dragLockMinDistance<=b.distance)&&(b.dragLockToAxis=!0);var l=e.lastEvent.direction;b.dragLockToAxis&&l!==b.direction&&(b.direction=r.isVertical(l)?b.deltaY<0?h:f:b.deltaX<0?g:i),c||(d.trigger(a+"start",b),c=!0),d.trigger(a,b),d.trigger(a+b.direction,b);var q=r.isVertical(b.direction);(d.options.dragBlockVertical&&q||d.options.dragBlockHorizontal&&!q)&&b.preventDefault();break;case p:c&&b.changedLength<=d.options.dragMaxTouches&&(d.trigger(a+"end",b),c=!1);break;case o:c=!1}}var c=!1;d.gestures.Drag={name:a,index:50,handler:b,defaults:{dragMinDistance:10,dragDistanceCorrection:!0,dragMaxTouches:1,dragBlockHorizontal:!1,dragBlockVertical:!1,dragLockToAxis:!1,dragLockMinDistance:25}}}("drag"),d.gestures.Gesture={name:"gesture",index:1337,handler:function(a,b){b.trigger(this.name,a)}},function(a){function b(b,d){var e=d.options,f=u.current;switch(b.eventType){case m:clearTimeout(c),f.name=a,c=setTimeout(function(){f&&f.name==a&&d.trigger(a,b)},e.holdTimeout);break;case n:b.distance>e.holdThreshold&&clearTimeout(c);break;case p:clearTimeout(c)}}var c;d.gestures.Hold={name:a,index:10,defaults:{holdTimeout:500,holdThreshold:2},handler:b}}("hold"),d.gestures.Release={name:"release",index:1/0,handler:function(a,b){a.eventType==p&&b.trigger(this.name,a)}},d.gestures.Swipe={name:"swipe",index:40,defaults:{swipeMinTouches:1,swipeMaxTouches:1,swipeVelocityX:.6,swipeVelocityY:.6},handler:function(a,b){if(a.eventType==p){var c=a.touches.length,d=b.options;if(cd.swipeMaxTouches)return;(a.velocityX>d.swipeVelocityX||a.velocityY>d.swipeVelocityY)&&(b.trigger(this.name,a),b.trigger(this.name+a.direction,a))}}},function(a){function b(b,d){var e,f,g=d.options,h=u.current,i=u.previous;switch(b.eventType){case m:c=!1;break;case n:c=c||b.distance>g.tapMaxDistance;break;case o:!r.inStr(b.srcEvent.type,"cancel")&&b.deltaTimed.options.transformMinRotation&&d.trigger("rotate",b),e>d.options.transformMinScale&&(d.trigger("pinch",b),d.trigger("pinch"+(b.scale<1?"in":"out"),b));break;case p:c&&b.changedLength<2&&(d.trigger(a+"end",b),c=!1)}}var c=!1;d.gestures.Transform={name:a,index:45,defaults:{transformMinScale:.01,transformMinRotation:1},handler:b}}("transform"),"function"==typeof define&&define.amd?define(function(){return d}):"undefined"!=typeof module&&module.exports?module.exports=d:a.Hammer=d}(window); 9 | //# sourceMappingURL=hammer.min.map -------------------------------------------------------------------------------- /public/static/js/script.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This is a javascript file for omninotesweb 4 | ============ 5 | 6 | Author: Suraj patil 7 | Updated: January 2015 8 | keyCode: n-110 9 | */ 10 | 11 | $(document).ready(function(){ 12 | 13 | /*on() is used instead of click because click can be used only on static elements, and on() is to be used when you add 14 | elements dynamically*/ 15 | $('[data-toggle="tooltip"]').tooltip(); 16 | // 17 | // $('.items').openOnHover(function(){ 18 | // alert(); 19 | // }); 20 | 21 | $('.floating-action-icon-add').click(function(){ 22 | $('#addNoteModal').modal('show'); 23 | }); 24 | 25 | // $("#datepicker").datepicker(); 26 | 27 | $('#editCatFrmBtn').click(function(){ 28 | $('#EditForm').toggleClass('hidden') 29 | }); 30 | 31 | $('#searchFormBtn').click(function(){ 32 | $('#SearchForm').toggleClass('hidden') 33 | }); 34 | 35 | $('#toggleAddFileGrp').click(function(){ 36 | $('#file-group').toggleClass('hidden'); 37 | $('#toggleAddFileGrp').addClass('hidden') ; 38 | }); 39 | 40 | $("#noti").click( 41 | function(){ 42 | this.fadeOut(); 43 | } 44 | ); 45 | 46 | $('.btnMessage').click(function(){$('.notification').fadeOut()}) 47 | 48 | /*$( document ).keypress( 49 | function(event){ 50 | if ( event.which == 110 ) { //bind the 'n' key to add note 51 | $('#addNoteModal').modal('show'); 52 | } 53 | 54 | if (event.which==109 ) { //binds the 'm' key to show the navigation drawer 55 | $('.sidebar-toggle').click(); 56 | } 57 | } 58 | );*/ 59 | 60 | $("#addNoteBtn").on("click", function() { 61 | this.preventDefaults(); 62 | var task_id = $("#task-id").val(); 63 | $.ajax({ 64 | url: "/tasks/" + task_id, 65 | type: "POST", 66 | data: {'title':'randome note', 'content':'this and that'} 67 | }).done(function(res, status) { 68 | console.log(status, res); 69 | var response = res 70 | $("#timeline").append(response) 71 | }); 72 | }); 73 | 74 | 75 | //~ $('.toggle').on('click', function(){$(this).next().toggle();}); 76 | 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /public/static/js/sidebar.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the Sonatra package. 3 | * 4 | * (c) François Pluchino 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | /*global jQuery*/ 11 | /*global window*/ 12 | /*global navigator*/ 13 | /*global document*/ 14 | /*global CSSMatrix*/ 15 | /*global WebKitCSSMatrix*/ 16 | /*global MSCSSMatrix*/ 17 | /*global Hammer*/ 18 | /*global Sidebar*/ 19 | 20 | /** 21 | * @param {jQuery} $ 22 | * 23 | * @typedef {Sidebar} Sidebar 24 | */ 25 | (function ($) { 26 | 'use strict'; 27 | 28 | /** 29 | * Check if is a mobile device. 30 | * 31 | * @returns {boolean} 32 | * 33 | * @private 34 | */ 35 | function mobileCheck() { 36 | return Boolean(navigator.userAgent.match(/Android|iPhone|iPad|iPod|IEMobile|BlackBerry|Opera Mini/i)); 37 | } 38 | 39 | /** 40 | * Binding actions of keyboard. 41 | * 42 | * @param {jQuery.Event|Event} event 43 | * 44 | * @typedef {Sidebar} Event.data The sidebar instance 45 | * 46 | * @private 47 | */ 48 | function keyboardAction(event) { 49 | if (!event instanceof jQuery.Event || event.data.options.disabledKeyboard) { 50 | return; 51 | } 52 | 53 | var self = event.data, 54 | kbe = self.options.keyboardEvent; 55 | 56 | if (event.shiftKey === kbe.shiftKey 57 | && event.ctrlKey === kbe.ctrlKey 58 | && event.altKey === kbe.altKey 59 | && event.keyCode === kbe.keyCode) { 60 | self.toggle(event); 61 | } 62 | } 63 | 64 | /** 65 | * Checks if the window width is wider than the minimum width defined in 66 | * options. 67 | * 68 | * @param {Sidebar} self The sidebar instance 69 | * 70 | * @returns {boolean} 71 | * 72 | * @private 73 | */ 74 | function isOverMinWidth(self) { 75 | var scrollbarWidth = 'scrollbarWidth', 76 | isOver = false, 77 | $window = $(window), 78 | windowWidth = $window.innerWidth(), 79 | widthNoScroll, 80 | inner, 81 | outer; 82 | 83 | if ($('body').height() > $window.innerHeight()) { 84 | if (null === self.scrollbarWidth) { 85 | inner = document.createElement('div'); 86 | outer = document.createElement('div'); 87 | outer.style.visibility = 'hidden'; 88 | outer.style.width = '100px'; 89 | 90 | document.body.appendChild(outer); 91 | 92 | widthNoScroll = outer.offsetWidth; 93 | outer.style.overflow = 'scroll'; 94 | inner.style.width = '100%'; 95 | outer.appendChild(inner); 96 | 97 | self[scrollbarWidth] = widthNoScroll - inner.offsetWidth; 98 | 99 | outer.parentNode.removeChild(outer); 100 | } 101 | 102 | windowWidth += self[scrollbarWidth]; 103 | } 104 | 105 | if (windowWidth >= self.options.minLockWidth) { 106 | isOver = true; 107 | } 108 | 109 | return isOver; 110 | } 111 | 112 | /** 113 | * Close the sidebar since external action. 114 | * 115 | * @param {Event} event The event 116 | * 117 | * @typedef {Sidebar} Event.data The sidebar instance 118 | * 119 | * @private 120 | */ 121 | function closeExternal(event) { 122 | var self = event.data, 123 | $target = $(event.currentTarget.activeElement); 124 | 125 | if ((self.isLocked() && isOverMinWidth(self)) || $(event.target).parents('.' + self.options.classWrapper).size() > 0 || $target.parents('.' + self.options.classWrapper).size() > 0 || $target.hasClass('sidebar-swipe')) { 126 | return; 127 | } 128 | 129 | event.stopPropagation(); 130 | event.preventDefault(); 131 | 132 | if (isOverMinWidth(self)) { 133 | self.close(); 134 | 135 | } else { 136 | self.forceClose(); 137 | } 138 | } 139 | 140 | /** 141 | * Close the sidebar or reopen the locked sidebar on window resize event. 142 | * 143 | * @param {Event} event The event 144 | * 145 | * @typedef {Sidebar} Event.data The sidebar instance 146 | * 147 | * @private 148 | */ 149 | function onResizeWindow(event) { 150 | var self = event.data; 151 | 152 | if (isOverMinWidth(self) && self.isLocked()) { 153 | self.forceOpen(); 154 | 155 | return; 156 | } 157 | 158 | closeExternal(event); 159 | } 160 | 161 | /** 162 | * Get the sidebar wrapper position. 163 | * 164 | * @param {jQuery} $target 165 | * 166 | * @returns {number} The Y axis position 167 | * 168 | * @private 169 | */ 170 | function getWrapperPosition($target) { 171 | var transformCss = $target.css('transform'), 172 | transform = {e: 0, f: 0}, 173 | reMatrix, 174 | match; 175 | 176 | if (transformCss) { 177 | if ('function' === typeof CSSMatrix) { 178 | transform = new CSSMatrix(transformCss); 179 | 180 | } else if ('function' === typeof WebKitCSSMatrix) { 181 | transform = new WebKitCSSMatrix(transformCss); 182 | 183 | } else if ('function' === typeof MSCSSMatrix) { 184 | transform = new MSCSSMatrix(transformCss); 185 | 186 | } else { 187 | reMatrix = /matrix\(\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/; 188 | match = transformCss.match(reMatrix); 189 | 190 | if (match) { 191 | transform.e = parseInt(match[1], 10); 192 | transform.f = parseInt(match[2], 10); 193 | } 194 | } 195 | } 196 | 197 | return transform.e; 198 | } 199 | 200 | /** 201 | * Cleans the hammer configuration on the wrapper element. 202 | * 203 | * @param {Sidebar} self The sidebar instance 204 | * 205 | * @private 206 | */ 207 | function cleanHammer(self) { 208 | self.$wrapper.removeData('drap-start-position'); 209 | self.$wrapper.css('-webkit-transition', ''); 210 | self.$wrapper.css('transition', ''); 211 | self.$wrapper.css('-webkit-transform', ''); 212 | self.$wrapper.css('transform', ''); 213 | self.$wrapper.removeClass(self.options.classOnDragging); 214 | delete self.dragStartPosition; 215 | } 216 | 217 | /** 218 | * Action of "on drag" hammer event. 219 | * 220 | * @param {Sidebar} self The sidebar instance 221 | * @param {Event} event The hammer event 222 | * 223 | * @private 224 | */ 225 | function onDrag(self, event) { 226 | var dragStartPosition = 'dragStartPosition', 227 | horizontal; 228 | 229 | if (undefined !== self.hammerScroll) { 230 | self.hammerScroll.onDrag(event); 231 | } 232 | 233 | if ((Hammer.DIRECTION_LEFT !== event.gesture.direction && Hammer.DIRECTION_RIGHT !== event.gesture.direction) 234 | || (self.options.locked && isOverMinWidth(self))) { 235 | return; 236 | } 237 | 238 | if (undefined === self.dragStartPosition) { 239 | self[dragStartPosition] = getWrapperPosition(self.$wrapper); 240 | } 241 | 242 | horizontal = Math.round(self.dragStartPosition + event.gesture.deltaX); 243 | 244 | if ((Sidebar.POSITION_LEFT === self.getPosition() && horizontal > 0) || (Sidebar.POSITION_RIGHT === self.getPosition() && horizontal < 0)) { 245 | horizontal = 0; 246 | } 247 | 248 | self.$wrapper.addClass(self.options.classOnDragging); 249 | self.$wrapper.css('-webkit-transition', 'none'); 250 | self.$wrapper.css('transition', 'none'); 251 | self.$wrapper.css('-webkit-transform', 'translate3d(' + horizontal + 'px, 0px, 0px)'); 252 | self.$wrapper.css('transform', 'translate3d(' + horizontal + 'px, 0px, 0px)'); 253 | } 254 | 255 | /** 256 | * Action of "on drag end" hammer event. 257 | * 258 | * @param {Sidebar} self The sidebar instance 259 | * @param {Event} event The hammer event 260 | * 261 | * @private 262 | */ 263 | function onDragEnd(self, event) { 264 | var closeGesture = Sidebar.POSITION_LEFT, 265 | openGesture = Sidebar.POSITION_RIGHT; 266 | 267 | if (undefined !== self.hammerScroll) { 268 | self.hammerScroll.onDragEnd(event); 269 | } 270 | 271 | cleanHammer(self); 272 | 273 | if (Math.abs(event.gesture.deltaX) <= (self.$wrapper.innerWidth() / 4)) { 274 | return; 275 | } 276 | 277 | if (Sidebar.POSITION_RIGHT === self.getPosition()) { 278 | closeGesture = Sidebar.POSITION_RIGHT; 279 | openGesture = Sidebar.POSITION_LEFT; 280 | } 281 | 282 | if (self.isOpen() && closeGesture === event.gesture.direction) { 283 | self.forceClose(); 284 | 285 | } else if (openGesture === event.gesture.direction) { 286 | if (self.isOpen() && isOverMinWidth(self) && $.inArray(self.options.forceToggle, [true, 'always']) >= 0) { 287 | self.forceOpen(); 288 | 289 | } else if (isOverMinWidth(self) && 'always' === self.options.forceToggle) { 290 | self.forceOpen(); 291 | 292 | } else { 293 | self.open(); 294 | } 295 | } 296 | } 297 | 298 | /** 299 | * Init the hammer instance. 300 | * 301 | * @param {Sidebar} self The sidebar instance 302 | * 303 | * @private 304 | */ 305 | function initHammer(self) { 306 | if (!Hammer) { 307 | return; 308 | } 309 | 310 | var swipe = '$swipe', 311 | hammer = 'hammer', 312 | hammerScroll = 'hammerScroll'; 313 | 314 | if ($.fn.hammerScroll) { 315 | self[hammerScroll] = $('.sidebar-scroller', self.$wrapper).hammerScroll({ 316 | contentWrapperClass: 'sidebar-scroller-content', 317 | eventDelegated: true, 318 | hammerStickyHeader: self.options.sidebarStickyHeader, 319 | scrollbar: self.options.hammerScrollbar, 320 | scrollbarInverse: Sidebar.POSITION_RIGHT === self.options.position 321 | }).data('st.hammerscroll'); 322 | } 323 | 324 | self[swipe] = $('').appendTo(self.$element); 325 | self[swipe].on('mouseover', function (event) { 326 | event.stopPropagation(); 327 | event.preventDefault(); 328 | }); 329 | 330 | self[hammer] = new Hammer(self.$element[0], { 331 | tap: false, 332 | transform: false, 333 | release: false, 334 | hold: false, 335 | swipe: false, 336 | drag_block_horizontal: true, 337 | drag_block_vertical: $.fn.hammerScroll, 338 | drag_lock_to_axis: false, 339 | drag_min_distance: 5 340 | }) 341 | 342 | .on('drag', function (event) { 343 | onDrag(self, event); 344 | }) 345 | .on('dragend', function (event) { 346 | onDragEnd(self, event); 347 | }); 348 | } 349 | 350 | /** 351 | * Destroy the hammer configuration. 352 | * 353 | * @param {Sidebar} self The sidebar instance 354 | * 355 | * @private 356 | */ 357 | function destroyHammer(self) { 358 | if (!Hammer) { 359 | return; 360 | } 361 | 362 | self.$swipe.off('mouseover'); 363 | self.$element.remove(self.$swipe); 364 | } 365 | 366 | // SIDEBAR CLASS DEFINITION 367 | // ======================== 368 | 369 | /** 370 | * @constructor 371 | * 372 | * @param {string|elements|object|jQuery} element 373 | * @param {object} options 374 | * 375 | * @this Sidebar 376 | */ 377 | var Sidebar = function (element, options) { 378 | this.guid = jQuery.guid; 379 | this.options = $.extend({}, Sidebar.DEFAULTS, options); 380 | this.$element = $(element); 381 | this.$toggle = $('.' + this.options.classToggle, this.$element); 382 | this.$wrapper = $('.' + this.options.classWrapper, this.$element); 383 | this.eventType = mobileCheck() ? 'touchstart' : 'click'; 384 | this.scrollbarWidth = null; 385 | this.hammer = undefined; 386 | this.hammerScroll = undefined; 387 | this.$swipe = undefined; 388 | this.$element.attr('data-sidebar', 'true'); 389 | 390 | var $findToggle; 391 | 392 | if (Sidebar.POSITION_RIGHT !== this.options.position) { 393 | this.options.position = Sidebar.POSITION_LEFT; 394 | 395 | } else { 396 | this.$element.addClass('sidebar-right'); 397 | } 398 | 399 | if (this.$element.hasClass('sidebar-right')) { 400 | this.options.position = Sidebar.POSITION_RIGHT; 401 | } 402 | 403 | if (this.options.locked) { 404 | this.options.forceToggle = 'always'; 405 | this.$element.css('-webkit-transition', 'none'); 406 | this.$element.css('transition', 'none'); 407 | this.$wrapper.css('-webkit-transition', 'none'); 408 | this.$wrapper.css('transition', 'none'); 409 | this.$element.addClass(this.options.classLocked); 410 | this.$element.addClass(this.options.classForceOpen); 411 | this.$wrapper.addClass(this.options.classOpen + '-init'); 412 | } 413 | 414 | if (!mobileCheck() && this.options.openOnHover && null === this.options.toggleId) { 415 | this.$element.on('mouseover.st.sidebar' + this.guid, $.proxy(Sidebar.prototype.open, this)); 416 | this.$element.on('mouseout.st.sidebar' + this.guid, $.proxy(Sidebar.prototype.close, this)); 417 | } 418 | 419 | if (null !== this.options.toggleId) { 420 | $findToggle = $('#' + this.options.toggleId); 421 | 422 | if (1 === $findToggle.size()) { 423 | this.$toggle.remove(); 424 | this.$toggle = $findToggle; 425 | } 426 | 427 | } else { 428 | this.$element.addClass('sidebar-togglable'); 429 | } 430 | 431 | this.$toggle.on(this.eventType + '.st.sidebar' + this.guid, null, this, Sidebar.prototype.toggle); 432 | $(window).on('keyup.st.sidebar' + this.guid, null, this, keyboardAction); 433 | $(window).on('resize.st.sidebar' + this.guid, null, this, onResizeWindow); 434 | 435 | if (this.$wrapper.hasClass(this.options.classOpen + '-init')) { 436 | if (isOverMinWidth(this)) { 437 | this.$wrapper.addClass(this.options.classOpen); 438 | 439 | } else { 440 | this.$wrapper.removeClass(this.options.classOpen); 441 | } 442 | 443 | this.$wrapper.removeClass(this.options.classOpen + '-init'); 444 | } 445 | 446 | if (this.$wrapper.hasClass(this.options.classOpen)) { 447 | $(document).on(this.eventType + '.st.sidebar' + this.guid, null, this, closeExternal); 448 | } 449 | 450 | if (this.options.sidebarStickyHeader && $.fn.stickyHeader && !$.fn.hammerScroll) { 451 | this.stickyHeader = $('.sidebar-scroller', this.$wrapper).stickyHeader().data('st.stickyheader'); 452 | } 453 | 454 | initHammer(this); 455 | 456 | this.$element.css('-webkit-transition', ''); 457 | this.$element.css('transition', ''); 458 | this.$wrapper.css('-webkit-transition', ''); 459 | this.$wrapper.css('transition', ''); 460 | this.$wrapper.addClass('sidebar-ready'); 461 | }, 462 | old; 463 | 464 | /** 465 | * Defaults options. 466 | * 467 | * @type {object} 468 | */ 469 | Sidebar.DEFAULTS = { 470 | classToggle: 'sidebar-toggle', 471 | classWrapper: 'sidebar-wrapper', 472 | classOpen: 'sidebar-open', 473 | classLocked: 'sidebar-locked', 474 | classForceOpen: 'sidebar-force-open', 475 | classOnDragging: 'sidebar-dragging', 476 | openOnHover: false, 477 | forceToggle: false,//false, true, 'always' 478 | locked: false, 479 | position: Sidebar.POSITION_LEFT,//left, right 480 | minLockWidth: 992, 481 | toggleId: null, 482 | sidebarStickyHeader: false, 483 | hammerScrollbar: true, 484 | disabledKeyboard: false, 485 | keyboardEvent: { 486 | ctrlKey: true, 487 | shiftKey: false, 488 | altKey: true, 489 | keyCode: 'S'.charCodeAt(0) 490 | } 491 | }; 492 | 493 | /** 494 | * Left position. 495 | * 496 | * @type {string} 497 | */ 498 | Sidebar.POSITION_LEFT = 'left'; 499 | 500 | /** 501 | * Right position. 502 | * 503 | * @type {string} 504 | */ 505 | Sidebar.POSITION_RIGHT = 'right'; 506 | 507 | /** 508 | * Get sidebar position. 509 | * 510 | * @returns {string} The position (left or right) 511 | * 512 | * @this Sidebar 513 | */ 514 | Sidebar.prototype.getPosition = function () { 515 | return this.options.position; 516 | }; 517 | 518 | /** 519 | * Checks if sidebar is locked (always open). 520 | * 521 | * @returns {boolean} 522 | * 523 | * @this Sidebar 524 | */ 525 | Sidebar.prototype.isLocked = function () { 526 | return this.options.locked; 527 | }; 528 | 529 | /** 530 | * Checks if sidebar is locked (always open). 531 | * 532 | * @returns {boolean} 533 | * 534 | * @this Sidebar 535 | */ 536 | Sidebar.prototype.isOpen = function () { 537 | return this.$wrapper.hasClass(this.options.classOpen); 538 | }; 539 | 540 | /** 541 | * Checks if sidebar is fully opened. 542 | * 543 | * @return {boolean} 544 | * 545 | * @this Sidebar 546 | */ 547 | Sidebar.prototype.isFullyOpened = function () { 548 | return this.$element.hasClass(this.options.classForceOpen); 549 | }; 550 | 551 | /** 552 | * Force open the sidebar. 553 | * 554 | * @this Sidebar 555 | */ 556 | Sidebar.prototype.forceOpen = function () { 557 | if (this.isOpen() && this.isFullyOpened()) { 558 | return; 559 | } 560 | 561 | this.$element.addClass(this.options.classForceOpen); 562 | this.open(); 563 | this.$toggle.removeClass(this.options.classToggle + '-opened'); 564 | }; 565 | 566 | /** 567 | * Force close the sidebar. 568 | * 569 | * @this Sidebar 570 | */ 571 | Sidebar.prototype.forceClose = function () { 572 | if (!this.isOpen() || (this.isLocked() && isOverMinWidth(this))) { 573 | return; 574 | } 575 | 576 | this.$element.removeClass(this.options.classForceOpen); 577 | this.close(); 578 | }; 579 | 580 | /** 581 | * Open the sidebar. 582 | * 583 | * @this Sidebar 584 | */ 585 | Sidebar.prototype.open = function () { 586 | if (this.isOpen()) { 587 | return; 588 | } 589 | 590 | $('[data-sidebar=true]').sidebar('forceClose'); 591 | this.$wrapper.addClass(this.options.classOpen); 592 | this.$toggle.addClass(this.options.classToggle + '-opened'); 593 | $(document).on(this.eventType + '.st.sidebar' + this.guid, null, this, closeExternal); 594 | }; 595 | 596 | /** 597 | * Close open the sidebar. 598 | * 599 | * @this Sidebar 600 | */ 601 | Sidebar.prototype.close = function () { 602 | if (!this.isOpen() || (this.isFullyOpened() && isOverMinWidth(this))) { 603 | return; 604 | } 605 | 606 | this.$wrapper.removeClass(this.options.classOpen); 607 | this.$toggle.removeClass(this.options.classToggle + '-opened'); 608 | $(document).off(this.eventType + '.st.sidebar' + this.guid, closeExternal); 609 | }; 610 | 611 | /** 612 | * Toggle the sidebar ("close, "open", "force open"). 613 | * 614 | * @param {jQuery.Event|Event} [event] 615 | * 616 | * @typedef {Sidebar} Event.data The sidebar instance 617 | * 618 | * @this Sidebar 619 | */ 620 | Sidebar.prototype.toggle = function (event) { 621 | var self = (undefined !== event) ? event.data : this, 622 | $target, 623 | $parents; 624 | 625 | if (event) { 626 | $target = $(event.target); 627 | $parents = $target.parents('.' + self.options.classWrapper); 628 | event.stopPropagation(); 629 | 630 | if ($target.hasClass(self.options.classToggle) || $target.parents('.' + self.options.classToggle).size() > 0) { 631 | event.preventDefault(); 632 | } 633 | 634 | if ($parents.size() > 0 || $target.hasClass('sidebar-swipe')) { 635 | return; 636 | } 637 | } 638 | 639 | if (self.isOpen()) { 640 | if (self.isFullyOpened()) { 641 | self.forceClose(); 642 | 643 | } else if (isOverMinWidth(self) && $.inArray(self.options.forceToggle, [true, 'always']) >= 0) { 644 | self.forceOpen(); 645 | 646 | } else { 647 | self.close(); 648 | } 649 | 650 | } else if (isOverMinWidth(self) && 'always' === self.options.forceToggle) { 651 | self.forceOpen(); 652 | 653 | } else { 654 | self.open(); 655 | } 656 | }; 657 | 658 | /** 659 | * Destroy instance. 660 | * 661 | * @this Sidebar 662 | */ 663 | Sidebar.prototype.destroy = function () { 664 | if (!mobileCheck()) { 665 | this.$element.off('mouseover.st.sidebar' + this.guid, $.proxy(Sidebar.prototype.open, this)); 666 | this.$element.off('mouseout.st.sidebar' + this.guid, $.proxy(Sidebar.prototype.close, this)); 667 | } 668 | 669 | $(document).off(this.eventType + '.st.sidebar' + this.guid, closeExternal); 670 | $(window).off('resize.st.sidebar' + this.guid, onResizeWindow); 671 | this.$toggle.off(this.eventType + '.st.sidebar' + this.guid, Sidebar.prototype.toggle); 672 | $(window).off('keyup.st.sidebar' + this.guid, keyboardAction); 673 | destroyHammer(this); 674 | 675 | if (undefined !== this.stickyHeader) { 676 | this.stickyHeader.destroy(); 677 | } 678 | 679 | this.$element.removeData('st.sidebar'); 680 | }; 681 | 682 | 683 | // SIDEBAR PLUGIN DEFINITION 684 | // ========================= 685 | 686 | function Plugin(option, value) { 687 | return this.each(function () { 688 | var $this = $(this), 689 | data = $this.data('st.sidebar'), 690 | options = typeof option === 'object' && option; 691 | 692 | if (!data && option === 'destroy') { 693 | return; 694 | } 695 | 696 | if (!data) { 697 | $this.data('st.sidebar', (data = new Sidebar(this, options))); 698 | } 699 | 700 | if (typeof option === 'string') { 701 | data[option](value); 702 | } 703 | }); 704 | } 705 | 706 | old = $.fn.sidebar; 707 | 708 | $.fn.sidebar = Plugin; 709 | $.fn.sidebar.Constructor = Sidebar; 710 | 711 | 712 | // SIDEBAR NO CONFLICT 713 | // =================== 714 | 715 | $.fn.sidebar.noConflict = function () { 716 | $.fn.sidebar = old; 717 | 718 | return this; 719 | }; 720 | 721 | 722 | // SIDEBAR DATA-API 723 | // ================ 724 | 725 | $(window).on('load', function () { 726 | $('[data-sidebar="true"]').each(function () { 727 | var $this = $(this); 728 | Plugin.call($this, $this.data()); 729 | }); 730 | }); 731 | 732 | }(jQuery)); 733 | -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE task ( 2 | id integer primary key autoincrement, 3 | title varchar(100), 4 | content text, 5 | task_status_id references status(id), 6 | created_date timestamp, 7 | due_date timestamp, 8 | last_modified_at timestamp, 9 | finish_date timestamp, 10 | priority integer, 11 | cat_id references category(id), 12 | user_id references user(id), 13 | hide int); 14 | CREATE TABLE status ( 15 | id integer primary key autoincrement, 16 | status varchar(50) not null 17 | ); 18 | CREATE TABLE files( 19 | name varchar(1000) not null, 20 | autoName varchar(255) not null, 21 | user_id references user(id), 22 | created_date timestamp 23 | ); 24 | CREATE TABLE category( 25 | id integer primary key autoincrement, 26 | name varchar(1000) not null, 27 | user_id references user(id) 28 | ); 29 | CREATE TABLE comments( 30 | id integer primary key autoincrement, 31 | content ntext, 32 | taskID references task(id), 33 | created datetime, 34 | user_id references user(id) 35 | ); 36 | CREATE TABLE user ( 37 | id integer primary key autoincrement, 38 | username varchar(100), 39 | password varchar(1000), 40 | email varchar(100) 41 | ); 42 | 43 | insert into status(status) values('COMPLETE'); 44 | insert into status(status) values('PENDING'); 45 | insert into status(status) values('DELETED'); 46 | -------------------------------------------------------------------------------- /sessions/sessions.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/sessions" 7 | ) 8 | 9 | //Store the cookie store which is going to store session data in the cookie 10 | var Store = sessions.NewCookieStore([]byte("secret-password")) 11 | var session *sessions.Session 12 | 13 | //IsLoggedIn will check if the user has an active session and return True 14 | func IsLoggedIn(r *http.Request) bool { 15 | session, err := Store.Get(r, "session") 16 | 17 | if err == nil && (session.Values["loggedin"] == "true") { 18 | return true 19 | } 20 | return false 21 | } 22 | 23 | //GetCurrentUserName returns the username of the logged in user 24 | func GetCurrentUserName(r *http.Request) string { 25 | session, err := Store.Get(r, "session") 26 | if err == nil { 27 | return session.Values["username"].(string) 28 | } 29 | return "" 30 | } 31 | -------------------------------------------------------------------------------- /templates/_foot.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /templates/_head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tasks 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |

${notification}

38 |
39 | 66 | 67 | 116 | 117 | 118 | 119 | 167 | -------------------------------------------------------------------------------- /templates/tasks.html: -------------------------------------------------------------------------------- 1 | {{template "_head.html" .}} 2 | 3 |
4 | 27 | 94 |
95 |
96 | {{template "_foot.html" .}} 97 | -------------------------------------------------------------------------------- /types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | /* 4 | Package types is used to store the context struct which 5 | is passed while templates are executed. 6 | */ 7 | //Task is the struct used to identify tasks 8 | type Task struct { 9 | Id int `json:"id"` 10 | Title string `json:"title"` 11 | Content string `json:"content"` 12 | Created string `json:"created"` 13 | Priority string `json:"priority"` 14 | Category string `json:"category"` 15 | Referer string `json:"referer,omitempty"` 16 | Comments []Comment `json:"comments,omitempty"` 17 | IsOverdue bool `json:"isoverdue, omitempty"` 18 | IsHidden int `json:"ishidden, omitempty` 19 | CompletedMsg string `json:"completedmsg, omitempty"` 20 | ShowComment bool `json:"showComment"` 21 | } 22 | 23 | type Tasks []Task 24 | 25 | //Comment is the struct used to populate comments per tasks 26 | type Comment struct { 27 | ID int `json:"id"` 28 | Content string `json:"content"` 29 | Created string `json:"created"` 30 | Username string `json:"author"` 31 | } 32 | 33 | //Context is the struct passed to templates 34 | type Context struct { 35 | Tasks []Task 36 | Navigation string 37 | Search string 38 | Message string 39 | CSRFToken string 40 | Categories []Category 41 | Referer string 42 | } 43 | 44 | //CategoryCount is the struct used to populate the sidebar 45 | //which contains the category name and the count of the tasks 46 | //in each category 47 | type CategoryCount struct { 48 | Name string 49 | Count int 50 | } 51 | 52 | //Status is the JSON struct to be returned 53 | type Status struct { 54 | StatusCode int `json:"status_code"` 55 | Message string `json:"message"` 56 | } 57 | 58 | //Category is the structure of the category table 59 | type Category struct { 60 | ID int `json:"categoryId"` 61 | Name string `json:"categoryName"` 62 | TaskCount int `json:"taskCount"` 63 | } 64 | 65 | //Categories will show 66 | type Categories []Category 67 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "strings" 4 | 5 | func GetRedirectUrl(referer string) string { 6 | var redirectUrl string 7 | url := strings.Split(referer, "/") 8 | 9 | if len(url) > 4 { 10 | redirectUrl = "/" + strings.Join(url[3:], "/") 11 | } else { 12 | redirectUrl = "/" 13 | } 14 | return redirectUrl 15 | } 16 | -------------------------------------------------------------------------------- /views/api.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "encoding/json" 5 | "html/template" 6 | "log" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/thewhitetulip/Tasks-vue/db" 12 | "github.com/thewhitetulip/Tasks-vue/sessions" 13 | "github.com/thewhitetulip/Tasks-vue/types" 14 | ) 15 | 16 | const unableToProcess = "Something went wrong" 17 | 18 | //GetTasksFuncAPI fetches tasks depending on the request, the authorization will be taken care by our middleare 19 | //in this function we will return all the tasks to the user or tasks per category 20 | //GET /tasks/ 21 | func GetTasksFuncAPI(w http.ResponseWriter, r *http.Request) { 22 | var strTaskID string 23 | var err error 24 | var task types.Task 25 | var tasks types.Tasks 26 | var status types.Status 27 | isError := false 28 | 29 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 30 | 31 | username := sessions.GetCurrentUserName(r) 32 | log.Println("token is valid " + username + " is logged in") 33 | 34 | strTaskID = r.URL.Path[len("/task/"):] 35 | //this is when we get a request for all the tasks for that user 36 | if strTaskID == "" { 37 | context, err := db.GetTasks(username, "pending", "") 38 | if err != nil { 39 | isError = true 40 | } else { 41 | tasks = context.Tasks 42 | for _, tasks := range tasks { 43 | tasks.ShowComment = false 44 | } 45 | w.WriteHeader(http.StatusOK) 46 | err = json.NewEncoder(w).Encode(tasks) 47 | 48 | if err != nil { 49 | panic(err) 50 | } 51 | return 52 | } 53 | } else { 54 | //this is when we get a request for a particular task 55 | taskID, err := strconv.Atoi(strTaskID) 56 | if err != nil { 57 | isError = true 58 | } else { 59 | ctx, err := db.GetTaskByID(username, taskID) 60 | task = ctx.Tasks[0] 61 | w.WriteHeader(http.StatusOK) 62 | err = json.NewEncoder(w).Encode(task) 63 | if err != nil { 64 | panic(err) 65 | } 66 | return 67 | } 68 | } 69 | 70 | if isError { 71 | log.Println("GetTasksFuncAPI: api.go: taskID") 72 | status = types.Status{http.StatusInternalServerError, unableToProcess} 73 | w.WriteHeader(http.StatusInternalServerError) 74 | err = json.NewEncoder(w).Encode(status) 75 | 76 | if err != nil { 77 | panic(err) 78 | } 79 | } 80 | } 81 | 82 | //AddTaskFuncAPI will add the tasks for the user 83 | func AddTaskFuncAPI(w http.ResponseWriter, r *http.Request) { 84 | var hidden = 0 // assume that it is not hidden, if form element is true, set this to 1 85 | 86 | statusCode := http.StatusOK 87 | message := "Task added to db" 88 | 89 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 90 | 91 | username := sessions.GetCurrentUserName(r) 92 | 93 | r.ParseForm() 94 | category := r.FormValue("category") 95 | title := template.HTMLEscapeString(r.Form.Get("title")) 96 | content := template.HTMLEscapeString(r.Form.Get("content")) 97 | taskPriority, priorityErr := strconv.Atoi(r.FormValue("priority")) 98 | hideTimeline := r.FormValue("ishidden") 99 | 100 | if priorityErr != nil { 101 | log.Print(priorityErr) 102 | message = "Bad task priority" 103 | } 104 | priorityList := []int{1, 2, 3} 105 | found := false 106 | for _, priority := range priorityList { 107 | if taskPriority == priority { 108 | found = true 109 | } 110 | } 111 | //If someone gives us incorrect priority number, we give the priority 112 | //to that task as 1 i.e. Low 113 | if !found { 114 | taskPriority = 1 115 | } 116 | 117 | if hideTimeline == "true" { 118 | hidden = 1 119 | } 120 | 121 | if title != "" && content != "" { 122 | taskTruth := db.AddTask(title, content, category, taskPriority, username, hidden) 123 | if taskTruth != nil { 124 | statusCode = http.StatusInternalServerError 125 | message = unableToProcess 126 | } 127 | } 128 | 129 | status := types.Status{statusCode, message} 130 | json.NewEncoder(w).Encode(status) 131 | } 132 | 133 | //UpdateTaskFuncAPI will add the tasks for the user 134 | func UpdateTaskFuncAPI(w http.ResponseWriter, r *http.Request) { 135 | taskErr := false 136 | statusCode := http.StatusOK 137 | var hidden = 0 138 | message := "updated task id " 139 | 140 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 141 | 142 | username := sessions.GetCurrentUserName(r) 143 | 144 | r.ParseForm() 145 | strID := r.Form.Get("id") 146 | id, err := strconv.Atoi(strID) 147 | if err != nil { 148 | log.Println(err) 149 | taskErr = true 150 | } 151 | 152 | category := r.Form.Get("category") 153 | title := r.Form.Get("title") 154 | content := r.Form.Get("content") 155 | priority, err := strconv.Atoi(r.Form.Get("priority")) 156 | hideTimeline := r.FormValue("ishidden") 157 | 158 | if err != nil { 159 | log.Println(err) 160 | priority = 1 161 | } 162 | 163 | if hideTimeline == "true" { 164 | hidden = 1 165 | } 166 | 167 | if strID != "" && title != "" && content != "" { 168 | err = db.UpdateTask(id, title, content, category, priority, username, hidden) 169 | if err != nil { 170 | taskErr = true 171 | } 172 | } else { 173 | taskErr = true 174 | } 175 | 176 | if taskErr { 177 | statusCode = http.StatusBadRequest 178 | message = unableToProcess 179 | } 180 | 181 | status := types.Status{statusCode, message} 182 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 183 | 184 | w.WriteHeader(http.StatusOK) 185 | 186 | err = json.NewEncoder(w).Encode(status) 187 | if err != nil { 188 | panic(err) 189 | } 190 | } 191 | 192 | //GetCompletedTaskFuncAPI will get the deleted tasks for the user 193 | func GetCompletedTaskFuncAPI(w http.ResponseWriter, r *http.Request) { 194 | var err error 195 | var tasks types.Tasks 196 | var status types.Status 197 | var statusCode = http.StatusOK 198 | 199 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 200 | 201 | username := sessions.GetCurrentUserName(r) 202 | 203 | //this is when we get a request for all the deleted tasks for that user 204 | context, err := db.GetTasks(username, "completed", "") 205 | 206 | if err != nil { 207 | log.Println("GetTasksFuncAPI: api.go: Server error") 208 | statusCode = http.StatusInternalServerError 209 | status = types.Status{statusCode, unableToProcess} 210 | w.WriteHeader(statusCode) 211 | err = json.NewEncoder(w).Encode(status) 212 | 213 | if err != nil { 214 | panic(err) 215 | } 216 | return 217 | } 218 | 219 | tasks = context.Tasks 220 | w.WriteHeader(statusCode) 221 | err = json.NewEncoder(w).Encode(tasks) 222 | 223 | if err != nil { 224 | panic(err) 225 | } 226 | return 227 | } 228 | 229 | //GetDeletedTaskFuncAPI will get the deleted tasks for the user 230 | func GetDeletedTaskFuncAPI(w http.ResponseWriter, r *http.Request) { 231 | var err error 232 | var tasks types.Tasks 233 | var status types.Status 234 | var statusCode = http.StatusOK 235 | 236 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 237 | username := sessions.GetCurrentUserName(r) 238 | log.Println("token is valid " + username + " is logged in") 239 | 240 | // this is when we get a request for all the deleted tasks for that user 241 | context, err := db.GetTasks(username, "deleted", "") 242 | if err != nil { 243 | log.Println("GetTasksFuncAPI: api.go: Server error") 244 | statusCode = http.StatusInternalServerError 245 | status = types.Status{statusCode, unableToProcess} 246 | w.WriteHeader(statusCode) 247 | err = json.NewEncoder(w).Encode(status) 248 | 249 | if err != nil { 250 | panic(err) 251 | } 252 | return 253 | } 254 | 255 | tasks = context.Tasks 256 | w.WriteHeader(statusCode) 257 | err = json.NewEncoder(w).Encode(tasks) 258 | 259 | if err != nil { 260 | panic(err) 261 | } 262 | return 263 | } 264 | 265 | //GetCategoryFuncAPI will return the categories for the user 266 | //depends on the ID that we get, if we get all, then return all categories of the user as a JSON. 267 | func GetCategoryFuncAPI(w http.ResponseWriter, r *http.Request) { 268 | statusCode := http.StatusOK 269 | 270 | username := sessions.GetCurrentUserName(r) 271 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 272 | 273 | categories, err := db.GetCategories(username) 274 | if err != nil { 275 | statusCode = http.StatusInternalServerError 276 | status := types.Status{statusCode, unableToProcess} 277 | w.WriteHeader(statusCode) 278 | err = json.NewEncoder(w).Encode(status) 279 | if err != nil { 280 | panic(err) 281 | } 282 | return 283 | } 284 | 285 | w.WriteHeader(statusCode) 286 | err = json.NewEncoder(w).Encode(categories) 287 | if err != nil { 288 | panic(err) 289 | } 290 | 291 | } 292 | 293 | //AddCategoryFuncAPI will add the category for the user 294 | func AddCategoryFuncAPI(w http.ResponseWriter, r *http.Request) { 295 | var err error 296 | var message = "Added Category" 297 | var statusCode = http.StatusOK 298 | 299 | r.ParseForm() 300 | 301 | category := r.Form.Get("categoryName") 302 | if strings.Trim(category, " ") != "" { 303 | username := sessions.GetCurrentUserName(r) 304 | log.Println("adding category") 305 | err := db.AddCategory(username, category) 306 | if err != nil { 307 | statusCode = http.StatusInternalServerError 308 | message = unableToProcess 309 | } 310 | } else { 311 | statusCode = http.StatusInternalServerError 312 | message = unableToProcess 313 | } 314 | 315 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 316 | w.WriteHeader(statusCode) 317 | status := types.Status{statusCode, message} 318 | err = json.NewEncoder(w).Encode(status) 319 | if err != nil { 320 | panic(err) 321 | } 322 | } 323 | 324 | //UpdateCategoryFuncAPI will update the category for the user 325 | func UpdateCategoryFuncAPI(w http.ResponseWriter, r *http.Request) { 326 | message := "Category Updated" 327 | statusCode := http.StatusOK 328 | var err error 329 | 330 | r.ParseForm() 331 | 332 | oldName := r.URL.Path[len("/category/"):] 333 | oldName = strings.Trim(oldName, "/") 334 | 335 | newName := r.Form.Get("newCategoryName") 336 | 337 | if strings.Trim(newName, " ") == "" { 338 | statusCode = http.StatusBadRequest 339 | message = unableToProcess 340 | } else { 341 | username := sessions.GetCurrentUserName(r) 342 | err = db.UpdateCategoryByName(username, oldName, newName) 343 | if err != nil { 344 | message = unableToProcess 345 | statusCode = http.StatusInternalServerError 346 | } 347 | } 348 | 349 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 350 | w.WriteHeader(statusCode) 351 | status := types.Status{statusCode, message} 352 | err = json.NewEncoder(w).Encode(status) 353 | if err != nil { 354 | panic(err) 355 | } 356 | } 357 | 358 | //DeleteCategoryFuncAPI will delete the category for the user 359 | func DeleteCategoryFuncAPI(w http.ResponseWriter, r *http.Request) { 360 | var err error 361 | message := "Category Deleted" 362 | var statusCode = http.StatusOK 363 | 364 | categoryName := r.URL.Path[len("/category/"):] 365 | categoryName = strings.Trim(categoryName, "/") 366 | categoryName = strings.Trim(categoryName, " ") 367 | 368 | if categoryName == "" { 369 | message = unableToProcess 370 | } else { 371 | username := sessions.GetCurrentUserName(r) 372 | err = db.DeleteCategoryByName(username, categoryName) 373 | if err != nil { 374 | message = unableToProcess 375 | statusCode = http.StatusInternalServerError 376 | log.Println(err) 377 | } 378 | } 379 | 380 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 381 | w.WriteHeader(statusCode) 382 | status := types.Status{statusCode, message} 383 | err = json.NewEncoder(w).Encode(status) 384 | if err != nil { 385 | panic(err) 386 | } 387 | } 388 | 389 | // ShowCategoryFuncAPI will return all the tasks of a particular category 390 | // we will be returning a status internal server error in case we do not find the 391 | // tasks of that category, the url it will handle is GET /categories/; if value is nil it'll return a JSON error 392 | func ShowCategoryFuncAPI(w http.ResponseWriter, r *http.Request) { 393 | message := "Success" 394 | statusCode := http.StatusOK 395 | 396 | category := r.URL.Path[len("/category/"):] 397 | 398 | if category == "" { 399 | statusCode = http.StatusBadRequest 400 | message = unableToProcess 401 | } else { 402 | username := sessions.GetCurrentUserName(r) 403 | log.Println("fetching tasks for " + category) 404 | context, err := db.GetTasks(username, "", category) 405 | if err != nil { 406 | log.Println("ShowCategoryFuncAPI: api.go: Server error") 407 | message = unableToProcess 408 | statusCode = http.StatusInternalServerError 409 | log.Println(message) 410 | } else { 411 | err = json.NewEncoder(w).Encode(context.Tasks) 412 | if err != nil { 413 | panic(err) 414 | } 415 | return 416 | } 417 | 418 | /*for i := 0; i < len(context.Tasks); i++ { 419 | context.Tasks[i].Content = string(md.Markdown([]byte(context.Tasks[i].Content))) 420 | }*/ 421 | 422 | } 423 | 424 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 425 | w.WriteHeader(statusCode) 426 | status := types.Status{statusCode, message} 427 | err = json.NewEncoder(w).Encode(status) 428 | if err != nil { 429 | panic(err) 430 | } 431 | 432 | } 433 | 434 | //CompleteTaskFuncAPI will delete a task which is passed as an ID 435 | func CompleteTaskFuncAPI(w http.ResponseWriter, r *http.Request) { 436 | statusCode := http.StatusOK 437 | message := "Task completed" 438 | 439 | id, err := strconv.Atoi(r.URL.Path[len("/complete-task/"):]) 440 | if err != nil { 441 | log.Println("CompleteTaskFuncAPI", err) 442 | message = unableToProcess 443 | statusCode = http.StatusBadRequest 444 | } else { 445 | username := sessions.GetCurrentUserName(r) 446 | err = db.CompleteTask(username, id) 447 | if err != nil { 448 | message = unableToProcess 449 | } 450 | } 451 | 452 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 453 | w.WriteHeader(statusCode) 454 | status := types.Status{statusCode, message} 455 | err = json.NewEncoder(w).Encode(status) 456 | if err != nil { 457 | panic(err) 458 | } 459 | return 460 | } 461 | 462 | func RestoreTaskFuncAPI(w http.ResponseWriter, r *http.Request) { 463 | var statusCode = http.StatusOK 464 | var message = "Task restored from Trash" 465 | 466 | id, err := strconv.Atoi(r.URL.Path[len("/restore-task/"):]) 467 | 468 | if err != nil { 469 | log.Println("RestoreTaskFunAPI", err) 470 | message = unableToProcess 471 | statusCode = http.StatusBadRequest 472 | } else { 473 | username := sessions.GetCurrentUserName(r) 474 | err = db.RestoreTask(username, id) 475 | if err != nil { 476 | message = unableToProcess 477 | } 478 | } 479 | 480 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 481 | w.WriteHeader(statusCode) 482 | status := types.Status{statusCode, message} 483 | err = json.NewEncoder(w).Encode(status) 484 | if err != nil { 485 | panic(err) 486 | } 487 | return 488 | 489 | } 490 | 491 | // TrashTaskFuncAPI handles the GET /trash-task/ and trashes the ID passed in the URL. 492 | // return JSON {http status code, output of operation}. 493 | func TrashTaskFuncAPI(w http.ResponseWriter, r *http.Request) { 494 | var statusCode = http.StatusOK 495 | var message = "Task Trashed" 496 | 497 | id, err := strconv.Atoi(r.URL.Path[len("/task/"):]) 498 | if err != nil { 499 | log.Println("TrashTaskFunc", err) 500 | message = unableToProcess 501 | statusCode = http.StatusBadRequest 502 | } else { 503 | username := sessions.GetCurrentUserName(r) 504 | err = db.TrashTask(username, id) 505 | if err != nil { 506 | message = unableToProcess 507 | statusCode = http.StatusInternalServerError 508 | } 509 | } 510 | 511 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 512 | w.WriteHeader(statusCode) 513 | status := types.Status{statusCode, message} 514 | err = json.NewEncoder(w).Encode(status) 515 | if err != nil { 516 | panic(err) 517 | } 518 | return 519 | } 520 | 521 | // RestoreFromCompleteFuncAPI handles the GET /incomplete-task/ and restores the status of task 522 | // from complete to pending. 523 | // Returns a JSON {http status code, output} like {400, "Not Deleted"} or {200, "Comment deleted"}. 524 | // The status code is also written in the HTTP header of the response. 525 | func RestoreFromCompleteFuncAPI(w http.ResponseWriter, r *http.Request) { 526 | var statusCode = http.StatusOK 527 | var message = "Marked Incomplete" 528 | 529 | id, err := strconv.Atoi(r.URL.Path[len("/incomplete-task/"):]) 530 | 531 | if err != nil { 532 | log.Println("api.go: RestoreFromComplete", err) 533 | message = unableToProcess 534 | statusCode = http.StatusBadRequest 535 | } else { 536 | username := sessions.GetCurrentUserName(r) 537 | err = db.RestoreTaskFromComplete(username, id) 538 | if err != nil { 539 | message = unableToProcess 540 | } 541 | } 542 | 543 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 544 | w.WriteHeader(statusCode) 545 | status := types.Status{statusCode, message} 546 | err = json.NewEncoder(w).Encode(status) 547 | if err != nil { 548 | panic(err) 549 | } 550 | return 551 | } 552 | 553 | // AddCommentFuncAPI handles the PUT /comment/ and adds a new comment to the database. 554 | // Each comment has a parent task, comments can't be parents of comments. 555 | // Returns a JSON {http status code, output} like {400, "Unable to add comment"} or {200, "Comment Added"}. 556 | // The status code is also written in the HTTP header of the response. 557 | func AddCommentFuncAPI(w http.ResponseWriter, r *http.Request) { 558 | var statusCode = http.StatusOK 559 | var message = "Comment Added" 560 | 561 | r.ParseForm() 562 | text := r.Form.Get("content") 563 | taskID := r.Form.Get("taskID") 564 | var comment types.Comment 565 | 566 | taskIDInt, err := strconv.Atoi(taskID) 567 | 568 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 569 | 570 | if err != nil { 571 | message = unableToProcess 572 | statusCode = http.StatusBadRequest 573 | log.Println(err) 574 | status := types.Status{statusCode, message} 575 | w.WriteHeader(statusCode) 576 | err = json.NewEncoder(w).Encode(status) 577 | if err != nil { 578 | panic(err) 579 | } 580 | 581 | return 582 | } 583 | 584 | username := sessions.GetCurrentUserName(r) 585 | comment, err = db.AddComments(username, taskIDInt, text) 586 | statusCode = http.StatusOK 587 | if err != nil { 588 | log.Println("unable to insert into db") 589 | message = unableToProcess 590 | statusCode = http.StatusInternalServerError 591 | } 592 | w.WriteHeader(statusCode) 593 | err = json.NewEncoder(w).Encode(comment) 594 | if err != nil { 595 | panic(err) 596 | } 597 | 598 | } 599 | 600 | // DeleteCommentFuncAPI handles the DELETE /comment/12 and deletes a comment with ID 12. 601 | // Returns a JSON {http status code, output} like {400, "Not Deleted"} or {200, "Comment deleted"}. 602 | // The status code is also written in the HTTP header of the response. 603 | func DeleteCommentFuncAPI(w http.ResponseWriter, r *http.Request) { 604 | var statusCode = http.StatusOK 605 | var message = "Comment deleted" 606 | 607 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 608 | 609 | id := r.URL.Path[len("/comment/"):] 610 | 611 | commentID, err := strconv.Atoi(id) 612 | 613 | if err != nil { 614 | message = unableToProcess 615 | } else { 616 | username := sessions.GetCurrentUserName(r) 617 | err = db.DeleteCommentByID(username, commentID) 618 | if err != nil { 619 | message = unableToProcess 620 | statusCode = http.StatusInternalServerError 621 | } 622 | } 623 | 624 | w.WriteHeader(statusCode) 625 | status := types.Status{statusCode, message} 626 | err = json.NewEncoder(w).Encode(status) 627 | if err != nil { 628 | panic(err) 629 | } 630 | return 631 | } 632 | -------------------------------------------------------------------------------- /views/api_sessions.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/thewhitetulip/Tasks-vue/db" 9 | "github.com/thewhitetulip/Tasks-vue/sessions" 10 | "github.com/thewhitetulip/Tasks-vue/types" 11 | ) 12 | 13 | //RequiresLogin is a middleware which will be used for each httpHandler to check if there is any active session 14 | func RequiresLogin(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { 15 | return func(w http.ResponseWriter, r *http.Request) { 16 | if !sessions.IsLoggedIn(r) { 17 | http.Redirect(w, r, "/login/", 302) 18 | return 19 | } 20 | handler(w, r) 21 | } 22 | } 23 | 24 | //LogoutFuncAPI Implements the logout functionality. WIll delete the session information from the cookie store 25 | func LogoutFuncAPI(w http.ResponseWriter, r *http.Request) { 26 | var status types.Status 27 | var message = "Login successful" 28 | htStatus := http.StatusOK 29 | 30 | session, err := sessions.Store.Get(r, "session") 31 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 32 | if err != nil { 33 | htStatus = http.StatusInternalServerError 34 | message = "Could not logout" 35 | } else { 36 | if session.Values["loggedin"] != "false" { 37 | session.Values["loggedin"] = "false" 38 | session.Save(r, w) 39 | } 40 | } 41 | w.WriteHeader(htStatus) 42 | err = json.NewEncoder(w).Encode(status) 43 | 44 | status = types.Status{htStatus, message} 45 | if err != nil { 46 | panic(err) 47 | } 48 | } 49 | 50 | //LoginFuncAPI implements the login functionality, will add a cookie to the cookie store for managing authentication 51 | func LoginFuncAPI(w http.ResponseWriter, r *http.Request) { 52 | var status types.Status 53 | var message string 54 | var htStatus = http.StatusOK 55 | 56 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 57 | 58 | session, err := sessions.Store.Get(r, "session") 59 | 60 | if r.Method == "GET" { 61 | value := (session.Values["loggedin"] == "true") 62 | w.WriteHeader(htStatus) 63 | type loggedIn struct { 64 | Loggedin bool `json:"loggedin"` 65 | } 66 | logged := loggedIn{Loggedin: value} 67 | json.NewEncoder(w).Encode(logged) 68 | return 69 | } 70 | 71 | if err != nil { 72 | log.Println("error identifying session") 73 | htStatus = http.StatusInternalServerError 74 | message = "Could not login" 75 | } else { 76 | 77 | r.ParseForm() 78 | username := r.Form.Get("username") 79 | password := r.Form.Get("password") 80 | 81 | if (username != "" && password != "") && db.ValidUser(username, password) { 82 | session.Values["loggedin"] = "true" 83 | session.Values["username"] = username 84 | session.Save(r, w) 85 | log.Print("user ", username, " is authenticated") 86 | message = "Logged in successfully" 87 | } else { 88 | htStatus = http.StatusBadRequest 89 | message = "Invalid user name or password;" 90 | } 91 | } 92 | 93 | w.WriteHeader(htStatus) 94 | status = types.Status{htStatus, message} 95 | err = json.NewEncoder(w).Encode(status) 96 | 97 | if err != nil { 98 | panic(err) 99 | } 100 | } 101 | 102 | //SignUpFuncAPI will enable new users to sign up to our service 103 | func SignUpFuncAPI(w http.ResponseWriter, r *http.Request) { 104 | var status types.Status 105 | var message = "Sign up success" 106 | var statusCode = http.StatusOK 107 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 108 | 109 | r.ParseForm() 110 | username := r.Form.Get("username") 111 | password := r.Form.Get("password") 112 | email := r.Form.Get("email") 113 | 114 | if username != "" && password != "" && email != "" { 115 | log.Println(username, password, email) 116 | 117 | err := db.CreateUser(username, password, email) 118 | if err != nil { 119 | statusCode = http.StatusInternalServerError 120 | message = "Something went wront" 121 | } 122 | } else { 123 | statusCode = http.StatusBadRequest 124 | message = "Invalid input" 125 | } 126 | 127 | w.WriteHeader(statusCode) 128 | status = types.Status{statusCode, message} 129 | err = json.NewEncoder(w).Encode(status) 130 | 131 | if err != nil { 132 | panic(err) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /views/views.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "html/template" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | var homeTemplate *template.Template 10 | var templates *template.Template 11 | 12 | var message string //message will store the message to be shown as notification 13 | var err error 14 | 15 | // Home renders the basic html page to the Vue front end, the Vue front end will then 16 | // update the html content according to the user interactions by making AJAX calls 17 | // we have used Go language's templating mechanism to split templates into logical parts 18 | func Home(w http.ResponseWriter, r *http.Request) { 19 | homeTemplate.Execute(w, nil) 20 | } 21 | 22 | // PopulateTemplates parses all templates present in 23 | // the templates folder and returns a pointer in templates 24 | // we use templates variable to locate other templates 25 | func PopulateTemplates() { 26 | templates, err = template.ParseGlob("./templates/*.html") 27 | if err != nil { 28 | log.Println(err) 29 | } 30 | homeTemplate = templates.Lookup("tasks.html") 31 | 32 | } 33 | --------------------------------------------------------------------------------