summaryrefslogtreecommitdiff
path: root/example/todo/app
diff options
context:
space:
mode:
authorLi Haoyi <haoyi.sg@gmail.com>2018-08-12 22:18:39 +0800
committerLi Haoyi <haoyi.sg@gmail.com>2018-08-12 22:18:39 +0800
commitfd9c399db8c1c0d86cc65d5e1c41968b42a813d1 (patch)
tree8e8fc2875cb1c26f309384a9ca0ad72e1fa893f3 /example/todo/app
parent9bf8c31fa9321558d7d02f6a5b687cd55a924e7f (diff)
downloadcask-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.js70
-rw-r--r--example/todo/app/resources/todo/index.css378
-rw-r--r--example/todo/app/src/todo/TodoServer.scala179
-rw-r--r--example/todo/app/test/src/todo/TodoTest.scala22
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