├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── Package.swift ├── Procfile ├── README.md ├── Sources ├── Database.swift ├── Handlers.swift └── main.swift ├── database.sql ├── screenshot.png └── webroot ├── index.mustache ├── main.js └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/screenshots 64 | 65 | Packages/ 66 | *.xcodeproj 67 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 @vsouza. 2 | # Author: Vinicius Souza 3 | 4 | FROM perfectlysoft/ubuntu1510 5 | RUN /usr/src/Perfect-Ubuntu/install_swift.sh --sure 6 | ADD . /usr/src/perfect-todo-example 7 | WORKDIR /usr/src/perfect-todo-example 8 | RUN swift build --configuration release 9 | CMD .build/release/perfect-todo-example 10 | EXPOSE 8181 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Vinicius Souza 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP_NAME=perfect-todo-example 2 | 3 | .PHONY: build 4 | build: 5 | @swift build 6 | 7 | .PHONY: build 8 | build-release: 9 | @swift build --configuration release 10 | 11 | .PHONY: run 12 | run:build 13 | @./.build/debug/$(APP_NAME) 14 | 15 | .PHONY: run-release 16 | run-release:build-releae 17 | @./.build/release/$(APP_NAME) 18 | 19 | .PHONY: generate-xcode 20 | generate-xcode: 21 | @swift package generate-xcodeproj 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "perfect-todo-example", 5 | targets: [], 6 | dependencies: [ 7 | .Package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", majorVersion: 2, minor: 0), 8 | .Package(url: "https://github.com/PerfectlySoft/Perfect-Mustache.git", majorVersion: 2, minor: 0), 9 | .Package(url:"https://github.com/PerfectlySoft/Perfect-MySQL.git", majorVersion: 2, minor: 0) 10 | ] 11 | ) 12 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: perfect-todo-example --port $PORT 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Perfect Todo List Example 2 | 3 | Swift 3.0 4 | Platforms OS X | Linux 5 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 6 | License MIT 7 | 8 | ![](screenshot.png) 9 | 10 | A simple todo list written with Swift [Perfect](http://perfect.org). 11 | 12 | This example illustrates how to create a simple todo list with Swift Perfect, persisting data on MySQL database. 13 | 14 | Check out the [live demo](http://bit.ly/2ebnw8T) running on AWS ElasticBeanstalk. 15 | 16 | ## Installation 17 | 18 | It's recommended to use Swift 3.0.1 19 | 20 | then setup your MySQL credentials on `Sources/Database.swift` 21 | 22 | ```swift 23 | let mysql = MySQL() 24 | let host = "127.0.0.1" 25 | let user = "root" 26 | let password = "" 27 | let db = "todo_perfect" 28 | let table = "todo" 29 | ``` 30 | 31 | 32 | `database.sql` file has instructions to create database and tables with sample data: 33 | 34 | example: `mysql -u yourusername -p yourpassword yourdatabase 35 | < database.sql ` 36 | 37 | after all, just run: 38 | 39 | `make run` 40 | 41 | ## License 42 | 43 | [MIT License](http://vsouza.mit-license.org/) © Vinicius Souza 44 | -------------------------------------------------------------------------------- /Sources/Database.swift: -------------------------------------------------------------------------------- 1 | import MySQL 2 | 3 | class DB { 4 | var mysql = MySQL() 5 | let host = "127.0.0.1" 6 | let user = "root" 7 | let password = "null" 8 | let db = "todo_perfect" 9 | let table = "todo" 10 | var connected = false 11 | 12 | init() { 13 | self.mysql = MySQL() 14 | self.connected = self.mysql.connect(host: self.host, user: self.user, password: self.password, db: self.db) 15 | if !self.connected { 16 | print(mysql.errorMessage()) 17 | } 18 | } 19 | 20 | func insertData(task: String) -> Bool { 21 | let status = 0 22 | guard self.connected else { 23 | print(self.mysql.errorMessage()) 24 | return false 25 | } 26 | return self.mysql.query(statement: "INSERT INTO \(self.table) (task, status) VALUES ('\(task)', \(status))") 27 | } 28 | 29 | func updateData(id: String?, status: Int) -> Bool { 30 | 31 | guard let taskId = id else { 32 | return false 33 | } 34 | return self.mysql.query(statement: "UPDATE \(self.table) SET status=\(status) WHERE id=\(taskId)") 35 | } 36 | 37 | func fetchData() -> [[String: Any]]? { 38 | 39 | let querySuccess = self.mysql.query(statement: "SELECT * FROM todo ORDER BY id DESC LIMIT 10") 40 | guard querySuccess else { 41 | return nil 42 | } 43 | let results = self.mysql.storeResults()! 44 | var resultArray = [[String:Any]]() 45 | 46 | while let row = results.next() { 47 | var item = [String: Any]() 48 | item["id"] = row[0] 49 | item["task"] = row[1] 50 | item["status"] = row[2] == "1" ? "checked" : "" 51 | resultArray.append(item) 52 | } 53 | return resultArray 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Handlers.swift: -------------------------------------------------------------------------------- 1 | import PerfectLib 2 | import PerfectHTTP 3 | import PerfectMustache 4 | 5 | 6 | 7 | struct IndexHandler: MustachePageHandler{ 8 | 9 | func extendValuesForResponse(context contxt: MustacheWebEvaluationContext, collector: MustacheEvaluationOutputCollector) { 10 | let dbHandler = DB() 11 | var values = MustacheEvaluationContext.MapType() 12 | values["tasks"] = dbHandler.fetchData() 13 | contxt.extendValues(with: values) 14 | do { 15 | try contxt.requestCompleted(withCollector: collector) 16 | } catch { 17 | let response = contxt.webResponse 18 | response.status = .internalServerError 19 | response.appendBody(string: "\(error)") 20 | response.completed() 21 | } 22 | } 23 | } 24 | 25 | struct NewTaskHandler: MustachePageHandler{ 26 | 27 | func extendValuesForResponse(context contxt: MustacheWebEvaluationContext, collector: MustacheEvaluationOutputCollector) { 28 | let dbHandler = DB() 29 | let request = contxt.webRequest 30 | var values = MustacheEvaluationContext.MapType() 31 | if let taskName = request.param(name: "task_name"){ 32 | values["task-name"] = taskName 33 | let _ = dbHandler.insertData(task: taskName) 34 | } 35 | values["tasks"] = dbHandler.fetchData() 36 | contxt.extendValues(with: values) 37 | do { 38 | try contxt.requestCompleted(withCollector: collector) 39 | } catch { 40 | let response = contxt.webResponse 41 | response.status = .internalServerError 42 | response.appendBody(string: "\(error)") 43 | response.completed() 44 | } 45 | } 46 | } 47 | 48 | struct TaskDoneHandler: MustachePageHandler{ 49 | 50 | func extendValuesForResponse(context contxt: MustacheWebEvaluationContext, collector: MustacheEvaluationOutputCollector) { 51 | let dbHandler = DB() 52 | let request = contxt.webRequest 53 | let taskId = request.param(name: "id") 54 | var taskStatus: Int? 55 | if let status = request.param(name: "status") { 56 | taskStatus = Int(status) 57 | } 58 | let _ = dbHandler.updateData(id: taskId, status: taskStatus!) 59 | var values = MustacheEvaluationContext.MapType() 60 | values["tasks"] = dbHandler.fetchData() 61 | contxt.extendValues(with: values) 62 | do { 63 | try contxt.requestCompleted(withCollector: collector) 64 | } catch { 65 | let response = contxt.webResponse 66 | response.status = .internalServerError 67 | response.appendBody(string: "\(error)") 68 | response.completed() 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/main.swift: -------------------------------------------------------------------------------- 1 | import PerfectLib 2 | import PerfectHTTP 3 | import PerfectHTTPServer 4 | import PerfectMustache 5 | 6 | // Create HTTP server. 7 | let server = HTTPServer() 8 | 9 | // Register your own routes and handlers 10 | var routes = Routes() 11 | 12 | // Adding a route to handle the root list 13 | routes.add(method: .get, uri: "/", handler: { 14 | request, response in 15 | 16 | // Setting the response content type explicitly to text/html 17 | response.setHeader(.contentType, value: "text/html") 18 | // Setting the body response to the generated list via Mustache 19 | mustacheRequest( 20 | request: request, 21 | response: response, 22 | handler: IndexHandler(), 23 | templatePath: request.documentRoot + "/index.mustache" 24 | ) 25 | // Signalling that the request is completed 26 | response.completed() 27 | } 28 | ) 29 | 30 | // Adding a route to handle the root list 31 | routes.add(method: .post, uri: "/", handler: { 32 | request, response in 33 | 34 | // Setting the response content type explicitly to text/html 35 | response.setHeader(.contentType, value: "text/html") 36 | // Setting the body response to the generated list via Mustache 37 | 38 | mustacheRequest( 39 | request: request, 40 | response: response, 41 | handler: NewTaskHandler(), 42 | templatePath: request.documentRoot + "/index.mustache" 43 | ) 44 | 45 | // Signalling that the request is completed 46 | response.completed() 47 | } 48 | ) 49 | 50 | // Adding a route to handle the root list 51 | routes.add(method: .post, uri: "/done", handler: { 52 | request, response in 53 | 54 | // Setting the response content type explicitly to text/html 55 | response.setHeader(.contentType, value: "text/html") 56 | // Setting the body response to the generated list via Mustache 57 | 58 | mustacheRequest( 59 | request: request, 60 | response: response, 61 | handler: TaskDoneHandler(), 62 | templatePath: request.documentRoot + "/index.mustache" 63 | ) 64 | 65 | // Signalling that the request is completed 66 | response.completed() 67 | } 68 | ) 69 | // Add the routes to the server. 70 | server.addRoutes(routes) 71 | 72 | // Set a listen port of 8181 73 | server.serverPort = 8181 74 | 75 | // Set a document root. 76 | // This is optional. If you do not want to serve static content then do not set this. 77 | // Setting the document root will automatically add a static file handler for the route 78 | server.documentRoot = "./webroot" 79 | 80 | do { 81 | // Launch the HTTP server. 82 | try server.start() 83 | } catch PerfectError.networkError(let err, let msg) { 84 | print("Network error thrown: \(err) \(msg)") 85 | } 86 | -------------------------------------------------------------------------------- /database.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE todo_perfect; 2 | 3 | USE todo_perfect; 4 | CREATE TABLE todo (id INTEGER PRIMARY KEY AUTO_INCREMENT, task char(100) NOT NULL, status bool NOT NULL); 5 | 6 | INSERT INTO todo (task,status) VALUES ('Have fun with perfect',1); 7 | INSERT INTO todo (task,status) VALUES ('Clean my closet',0); 8 | INSERT INTO todo (task,status) VALUES ('Learn Swift',1); 9 | INSERT INTO todo (task,status) VALUES ('Left the dogs out',0); 10 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesome-labs/swift-perfect-todo-example/d26e0d5d56971b4a766a73cc5630f761a1c59a24/screenshot.png -------------------------------------------------------------------------------- /webroot/index.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Perfect Todo Example 5 | 6 | 7 | 8 | 9 | Fork me on GitHub 10 |
11 |
12 |

Perfect Todo Example

13 |
14 | 15 | 16 |
17 |
18 |
19 |
    20 | {{ #tasks }} 21 |
  • 22 |
    23 | 24 | 25 |
    26 |
  • 27 | {{ /tasks }} 28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /webroot/main.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | 3 | $(window).keydown(function(event){ 4 | if(event.keyCode == 13) { 5 | event.preventDefault(); 6 | return false; 7 | } 8 | }); 9 | 10 | $(".toggle").click(function(){ 11 | var taskId = $(this).attr("data-taskid"); 12 | var status = $(this).is(":checked") ? 1 : 0; 13 | console.log(status) 14 | $.ajax({ 15 | url: "/done?id=" + taskId + "&status=" + status, 16 | type: "post", 17 | success: function (response) { 18 | console.log("task-updated") 19 | }, 20 | error: function(jqXHR, textStatus, errorThrown) { 21 | console.log(textStatus, errorThrown); 22 | } 23 | }); 24 | }) 25 | 26 | $(".add").click(function(){ 27 | var data = $(".new-task").serializeArray() 28 | $.ajax({ 29 | url: "/", 30 | type: "post", 31 | data: data, 32 | success: function (response) { 33 | console.log("task-updated") 34 | }, 35 | error: function(jqXHR, textStatus, errorThrown) { 36 | console.log(textStatus, errorThrown); 37 | } 38 | }); 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /webroot/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | font: 14px 'Dosis', Helvetica, Arial, sans-serif; 9 | font-weight: 200; 10 | line-height: 1.4em; 11 | background: #f5f5f5; 12 | color: #4d4d4d; 13 | min-width: 230px; 14 | max-width: 800px; 15 | margin: 0 auto; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-font-smoothing: antialiased; 18 | font-smoothing: antialiased; 19 | } 20 | 21 | button, 22 | input[type="checkbox"] { 23 | outline: none; 24 | } 25 | 26 | button { 27 | margin: 0; 28 | padding: 0; 29 | border: 0; 30 | background: none; 31 | font-size: 100%; 32 | vertical-align: baseline; 33 | font-family: inherit; 34 | font-weight: inherit; 35 | color: inherit; 36 | -webkit-appearance: none; 37 | appearance: none; 38 | -webkit-font-smoothing: antialiased; 39 | -moz-font-smoothing: antialiased; 40 | font-smoothing: antialiased; 41 | } 42 | 43 | label { 44 | white-space: pre; 45 | word-break: break-word; 46 | padding: 15px 60px 15px 15px; 47 | margin-left: 45px; 48 | display: block; 49 | line-height: 1.2; 50 | transition: color 0.4s; 51 | } 52 | 53 | .toggle { 54 | text-align: center; 55 | width: 40px; 56 | /* auto, since non-WebKit browsers doesn't support input styling */ 57 | height: auto; 58 | position: absolute; 59 | top: 0; 60 | bottom: 0; 61 | margin: auto 0; 62 | border: none; /* Mobile Safari */ 63 | -webkit-appearance: none; 64 | appearance: none; 65 | } 66 | 67 | .toggle:after { 68 | content: url('data:image/svg+xml;utf8,'); 69 | } 70 | 71 | .toggle:checked:after { 72 | content: url('data:image/svg+xml;utf8,'); 73 | } 74 | 75 | .toggle { 76 | background: none; 77 | height: 40px; 78 | } 79 | 80 | li { 81 | position: relative; 82 | font-size: 24px; 83 | border-bottom: 1px solid #ededed; 84 | } 85 | 86 | button { 87 | margin: 0; 88 | padding: 0; 89 | border: 0; 90 | background: none; 91 | font-size: 100%; 92 | vertical-align: baseline; 93 | font-family: inherit; 94 | font-weight: inherit; 95 | color: inherit; 96 | -webkit-appearance: none; 97 | appearance: none; 98 | -webkit-font-smoothing: antialiased; 99 | -moz-font-smoothing: antialiased; 100 | font-smoothing: antialiased; 101 | } 102 | 103 | .add { 104 | cursor: pointer; 105 | margin: 3px; 106 | padding: 3px 7px; 107 | border: 1px solid transparent; 108 | border-radius: 3px; 109 | } 110 | 111 | .add:hover { 112 | border-color: rgba(175, 47, 47, 0.1); 113 | } 114 | 115 | .hidden { 116 | display: none; 117 | } 118 | 119 | .todoapp { 120 | background: #fff; 121 | margin: 130px 0 40px 0; 122 | position: relative; 123 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 124 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 125 | } 126 | 127 | .todoapp input::-webkit-input-placeholder { 128 | font-style: italic; 129 | font-weight: 300; 130 | color: #e6e6e6; 131 | } 132 | 133 | .todoapp input::-moz-placeholder { 134 | font-style: italic; 135 | font-weight: 300; 136 | color: #e6e6e6; 137 | } 138 | 139 | .todoapp input::input-placeholder { 140 | font-style: italic; 141 | font-weight: 300; 142 | color: #e6e6e6; 143 | } 144 | 145 | .todoapp h1 { 146 | position: absolute; 147 | top: -105px; 148 | width: 100%; 149 | font-size: 60px; 150 | font-weight: 200; 151 | text-align: center; 152 | color: #fd6c32; 153 | -webkit-text-rendering: optimizeLegibility; 154 | -moz-text-rendering: optimizeLegibility; 155 | text-rendering: optimizeLegibility; 156 | } 157 | 158 | .new-todo { 159 | position: relative; 160 | margin: 0; 161 | width: 90%; 162 | font-size: 24px; 163 | font-family: inherit; 164 | font-weight: inherit; 165 | line-height: 1.4em; 166 | border: 0; 167 | outline: none; 168 | color: inherit; 169 | padding: 6px; 170 | border: 1px solid #999; 171 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 172 | box-sizing: border-box; 173 | -webkit-font-smoothing: antialiased; 174 | -moz-font-smoothing: antialiased; 175 | font-smoothing: antialiased; 176 | } 177 | 178 | .new-todo { 179 | padding: 16px 16px 16px 60px; 180 | border: none; 181 | background: rgba(0, 0, 0, 0.003); 182 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 183 | } 184 | 185 | .main { 186 | position: relative; 187 | z-index: 2; 188 | border-top: 1px solid #e6e6e6; 189 | } 190 | 191 | .todo-list { 192 | margin: 0; 193 | padding: 0; 194 | list-style: none; 195 | } 196 | 197 | label { 198 | white-space: pre; 199 | word-break: break-word; 200 | padding: 15px 60px 15px 15px; 201 | margin-left: 45px; 202 | display: block; 203 | line-height: 1.2; 204 | transition: color 0.4s; 205 | } 206 | 207 | footer { 208 | position: absolute; 209 | bottom: 15px; 210 | left:0; 211 | width: 100%; 212 | height: 20px; 213 | text-align: center; 214 | } 215 | 216 | footer a { 217 | color: #fd6c32; 218 | font-size: 22px; 219 | } 220 | --------------------------------------------------------------------------------