diff options
author | Li Haoyi <haoyi.sg@gmail.com> | 2018-08-12 22:18:39 +0800 |
---|---|---|
committer | Li Haoyi <haoyi.sg@gmail.com> | 2018-08-12 22:18:39 +0800 |
commit | fd9c399db8c1c0d86cc65d5e1c41968b42a813d1 (patch) | |
tree | 8e8fc2875cb1c26f309384a9ca0ad72e1fa893f3 /example/todo/app | |
parent | 9bf8c31fa9321558d7d02f6a5b687cd55a924e7f (diff) | |
download | cask-fd9c399db8c1c0d86cc65d5e1c41968b42a813d1.tar.gz cask-fd9c399db8c1c0d86cc65d5e1c41968b42a813d1.tar.bz2 cask-fd9c399db8c1c0d86cc65d5e1c41968b42a813d1.zip |
auto-upload examples
Diffstat (limited to 'example/todo/app')
-rw-r--r-- | example/todo/app/resources/todo/app.js | 70 | ||||
-rw-r--r-- | example/todo/app/resources/todo/index.css | 378 | ||||
-rw-r--r-- | example/todo/app/src/todo/TodoServer.scala | 179 | ||||
-rw-r--r-- | example/todo/app/test/src/todo/TodoTest.scala | 22 |
4 files changed, 649 insertions, 0 deletions
diff --git a/example/todo/app/resources/todo/app.js b/example/todo/app/resources/todo/app.js new file mode 100644 index 0000000..b7b8437 --- /dev/null +++ b/example/todo/app/resources/todo/app.js @@ -0,0 +1,70 @@ +var state = "all"; + +var todoApp = document.getElementsByClassName("todoapp")[0]; +function postFetchUpdate(url){ + fetch(url, { + method: "POST", + }) + .then(function(response){ return response.text()}) + .then(function (text) { + todoApp.innerHTML = text; + initListeners() + }) +} + +function bindEvent(cls, url, endState){ + + document.getElementsByClassName(cls)[0].addEventListener( + "mousedown", + function(evt){ + postFetchUpdate(url) + if (endState) state = endState + } + ); +} + +function bindIndexedEvent(cls, func){ + Array.from(document.getElementsByClassName(cls)).forEach( function(elem) { + elem.addEventListener( + "mousedown", + function(evt){ + postFetchUpdate(func(elem.getAttribute("data-todo-index"))) + } + ) + }); +} + +function initListeners(){ + bindIndexedEvent( + "destroy", + function(index){return "/delete/" + state + "/" + index} + ); + bindIndexedEvent( + "toggle", + function(index){return "/toggle/" + state + "/" + index} + ); + bindEvent("toggle-all", "/toggle-all/" + state); + bindEvent("todo-all", "/list/all", "all"); + bindEvent("todo-active", "/list/active", "active"); + bindEvent("todo-completed", "/list/completed", "completed"); + bindEvent("clear-completed", "/clear-completed/" + state); + var newTodoInput = document.getElementsByClassName("new-todo")[0]; + newTodoInput.addEventListener( + "keydown", + function(evt){ + if (evt.keyCode === 13) { + fetch("/add/" + state, { + method: "POST", + body: newTodoInput.value + }) + .then(function(response){ return response.text()}) + .then(function (text) { + newTodoInput.value = ""; + todoApp.innerHTML = text; + initListeners() + }) + } + } + ); +} +initListeners()
\ No newline at end of file diff --git a/example/todo/app/resources/todo/index.css b/example/todo/app/resources/todo/index.css new file mode 100644 index 0000000..208a762 --- /dev/null +++ b/example/todo/app/resources/todo/index.css @@ -0,0 +1,378 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; + font-weight: 300; +} + +button, +input[type="checkbox"] { + outline: none; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + outline: none; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +label[for='toggle-all'] { + display: none; +} + +.toggle-all { + position: absolute; + top: -55px; + left: -12px; + width: 60px; + height: 34px; + text-align: center; + border: none; /* Mobile Safari */ +} + +.toggle-all:before { + content: '❯'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all:checked:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: 506px; + padding: 13px 17px 12px 17px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle:after { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>'); +} + +.todo-list li .toggle:checked:after { + content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>'); +} + +.todo-list li label { + white-space: pre-line; + word-break: break-all; + padding: 15px 60px 15px 15px; + margin-left: 45px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: '×'; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a.selected, +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; + position: relative; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } + + .toggle-all { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + -webkit-appearance: none; + appearance: none; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +}
\ No newline at end of file diff --git a/example/todo/app/src/todo/TodoServer.scala b/example/todo/app/src/todo/TodoServer.scala new file mode 100644 index 0000000..0c66895 --- /dev/null +++ b/example/todo/app/src/todo/TodoServer.scala @@ -0,0 +1,179 @@ +package app +import cask.internal.Router +import com.typesafe.config.ConfigFactory +import io.getquill.{SnakeCase, SqliteJdbcContext} +import scalatags.Text.all._ +import scalatags.Text.tags2 +object TodoServer extends cask.MainRoutes{ + val tmpDb = java.nio.file.Files.createTempDirectory("todo-cask-sqlite") + + object ctx extends SqliteJdbcContext( + SnakeCase, + ConfigFactory.parseString( + s"""{"driverClassName":"org.sqlite.JDBC","jdbcUrl":"jdbc:sqlite:$tmpDb/file.db"}""" + ) + ) + + class transactional extends cask.Decorator{ + class TransactionFailed(val value: Router.Result.Error) extends Exception + def wrapFunction(pctx: cask.ParamContext, delegate: Delegate): Returned = { + try ctx.transaction( + delegate(Map()) match{ + case Router.Result.Success(t) => Router.Result.Success(t) + case e: Router.Result.Error => throw new TransactionFailed(e) + } + ) + catch{case e: TransactionFailed => e.value} + } + } + + case class Todo(id: Int, checked: Boolean, text: String) + + ctx.executeAction( + """CREATE TABLE todo ( + | id INTEGER PRIMARY KEY AUTOINCREMENT, + | checked BOOLEAN, + | text TEXT + |); + |""".stripMargin + ) + ctx.executeAction( + """INSERT INTO todo (checked, text) VALUES + |(1, 'Get started with Cask'), + |(0, 'Profit!'); + |""".stripMargin + ) + + import ctx._ + + @transactional + @cask.post("/list/:state") + def list(state: String) = renderBody(state).render + + @transactional + @cask.post("/add/:state") + def add(state: String, request: cask.Request) = { + val body = new String(request.data.readAllBytes()) + run(query[Todo].insert(_.checked -> lift(false), _.text -> lift(body)).returning(_.id)) + renderBody(state).render + } + + @transactional + @cask.post("/delete/:state/:index") + def delete(state: String, index: Int) = { + run(query[Todo].filter(_.id == lift(index)).delete) + renderBody(state).render + } + + @transactional + @cask.post("/toggle/:state/:index") + def toggle(state: String, index: Int) = { + run(query[Todo].filter(_.id == lift(index)).update(p => p.checked -> !p.checked)) + renderBody(state).render + } + + @transactional + @cask.post("/clear-completed/:state") + def clearCompleted(state: String) = { + run(query[Todo].filter(_.checked).delete) + renderBody(state).render + } + + @transactional + @cask.post("/toggle-all/:state") + def toggleAll(state: String) = { + val next = run(query[Todo].filter(_.checked).size) != 0 + run(query[Todo].update(_.checked -> !lift(next))) + renderBody(state).render + } + + def renderBody(state: String) = { + val filteredTodos = state match{ + case "all" => run(query[Todo]).sortBy(-_.id) + case "active" => run(query[Todo].filter(!_.checked)).sortBy(-_.id) + case "completed" => run(query[Todo].filter(_.checked)).sortBy(-_.id) + } + frag( + header(cls := "header", + h1("todos"), + input(cls := "new-todo", placeholder := "What needs to be done?", autofocus := "") + ), + tags2.section(cls := "main", + input( + id := "toggle-all", + cls := "toggle-all", + `type` := "checkbox", + if (run(query[Todo].filter(_.checked).size != 0)) checked else () + ), + label(`for` := "toggle-all","Mark all as complete"), + ul(cls := "todo-list", + for(todo <- filteredTodos) yield li( + if (todo.checked) cls := "completed" else (), + div(cls := "view", + input( + cls := "toggle", + `type` := "checkbox", + if (todo.checked) checked else (), + data("todo-index") := todo.id + ), + label(todo.text), + button(cls := "destroy", data("todo-index") := todo.id) + ), + input(cls := "edit", value := todo.text) + ) + ) + ), + footer(cls := "footer", + span(cls := "todo-count", + strong(run(query[Todo].filter(!_.checked).size).toInt), + " items left" + ), + ul(cls := "filters", + li(cls := "todo-all", + a(if (state == "all") cls := "selected" else (), "All") + ), + li(cls := "todo-active", + a(if (state == "active") cls := "selected" else (), "Active") + ), + li(cls := "todo-completed", + a(if (state == "completed") cls := "selected" else (), "Completed") + ) + ), + button(cls := "clear-completed","Clear completed") + ) + ) + } + + @transactional + @cask.get("/") + def index() = { + cask.Response( + "<!doctype html>" + html(lang := "en", + head( + meta(charset := "utf-8"), + meta(name := "viewport", content := "width=device-width, initial-scale=1"), + tags2.title("Template • TodoMVC"), + link(rel := "stylesheet", href := "/static/index.css") + ), + body( + tags2.section(cls := "todoapp", renderBody("all")), + footer(cls := "info", + p("Double-click to edit a todo"), + p("Created by ", + a(href := "http://todomvc.com","Li Haoyi") + ), + p("Part of ", + a(href := "http://todomvc.com","TodoMVC") + ) + ), + script(src := "/static/app.js") + ) + ) + ) + } + + @cask.static("/static") + def static() = "example/todo/resources/todo" + + initialize() +} diff --git a/example/todo/app/test/src/todo/TodoTest.scala b/example/todo/app/test/src/todo/TodoTest.scala new file mode 100644 index 0000000..8f38612 --- /dev/null +++ b/example/todo/app/test/src/todo/TodoTest.scala @@ -0,0 +1,22 @@ +package app +import utest._ +object TodoTests extends TestSuite{ + def test[T](example: cask.main.BaseMain)(f: String => T): T = { + val server = io.undertow.Undertow.builder + .addHttpListener(8080, "localhost") + .setHandler(example.defaultHandler) + .build + server.start() + val res = + try f("http://localhost:8080") + finally server.stop() + res + } + val tests = Tests{ + 'TodoServer - test(TodoServer){ host => + val page = requests.get(host).text() + assert(page.contains("What needs to be done?")) + } + } + +}
\ No newline at end of file |