commit 660e7a18601f33df15fa02ce09b13ae344c0ba7c
Author: Philippe PITTOLI
Date: Wed Nov 20 19:22:51 2019 +0100
first commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..41cb870
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules/*
+package-lock.json
+
+# generated files
+*.js
+*.css
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..855a0da
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,153 @@
+PACKAGE = 'todo-webclient'
+VERSION = '0.1'
+
+PREFIX := /usr/local
+BINDIR := $(PREFIX)/bin
+LIBDIR := $(PREFIX)/lib
+SHAREDIR := $(PREFIX)/share
+INCLUDEDIR := $(PREFIX)/include
+MANDIR := $(SHAREDIR)/man
+
+Q := @
+
+all: main.js style.css
+ @:
+
+main.js: main.bundle.js
+ @echo '[01;31m MIN > [01;37mmain.js[00m'
+ $(Q)npx babel --minified main.bundle.js -o main.js
+
+
+main.bundle.js: client/index.ls client/authd.ls client/bulma.ls client/todod.ls
+ @echo '[01;32m BUN > [01;37mmain.bundle.js[00m'
+ $(Q)npx browserify -t browserify-livescript client/index.ls -o main.bundle.js
+
+
+main.js.install: main.js
+ @echo '[01;31m IN > [01;37m$(SHAREDIR)/todo-webclient/main.js[00m'
+ $(Q)mkdir -p '$(DESTDIR)$(SHAREDIR)/todo-webclient'
+ $(Q)install -m644 main.js $(DESTDIR)$(SHAREDIR)/todo-webclient/main.js
+
+main.js.clean:
+ @echo '[01;37m RM > [01;37mmain.js[00m'
+ $(Q)rm -f main.js
+ @echo '[01;37m RM > [01;37mmain.bundle.js[00m'
+ $(Q)rm -f main.bundle.js
+
+
+main.js.uninstall:
+ @echo '[01;37m RM > [01;37m$(SHAREDIR)/todo-webclient/main.js[00m'
+ $(Q)rm -f '$(DESTDIR)$(SHAREDIR)/todo-webclient/main.js'
+
+style.css: client/style.sass
+ @echo '[01;33m CSS > [01;37mstyle.css[00m'
+ $(Q)sassc client/style.sass > style.css
+
+
+style.css.install: style.css
+ @echo '[01;31m IN > [01;37m$(SHAREDIR)/todo-webclient/style.css[00m'
+ $(Q)mkdir -p '$(DESTDIR)$(SHAREDIR)/todo-webclient'
+ $(Q)install -m644 style.css $(DESTDIR)$(SHAREDIR)/todo-webclient/style.css
+
+style.css.clean:
+ @echo '[01;37m RM > [01;37mstyle.css[00m'
+ $(Q)rm -f style.css
+
+style.css.uninstall:
+ @echo '[01;37m RM > [01;37m$(SHAREDIR)/todo-webclient/style.css[00m'
+ $(Q)rm -f '$(DESTDIR)$(SHAREDIR)/todo-webclient/style.css'
+
+$(DESTDIR)$(PREFIX):
+ @echo '[01;35m DIR > [01;37m$(PREFIX)[00m'
+ $(Q)mkdir -p $(DESTDIR)$(PREFIX)
+$(DESTDIR)$(BINDIR):
+ @echo '[01;35m DIR > [01;37m$(BINDIR)[00m'
+ $(Q)mkdir -p $(DESTDIR)$(BINDIR)
+$(DESTDIR)$(LIBDIR):
+ @echo '[01;35m DIR > [01;37m$(LIBDIR)[00m'
+ $(Q)mkdir -p $(DESTDIR)$(LIBDIR)
+$(DESTDIR)$(SHAREDIR):
+ @echo '[01;35m DIR > [01;37m$(SHAREDIR)[00m'
+ $(Q)mkdir -p $(DESTDIR)$(SHAREDIR)
+$(DESTDIR)$(INCLUDEDIR):
+ @echo '[01;35m DIR > [01;37m$(INCLUDEDIR)[00m'
+ $(Q)mkdir -p $(DESTDIR)$(INCLUDEDIR)
+$(DESTDIR)$(MANDIR):
+ @echo '[01;35m DIR > [01;37m$(MANDIR)[00m'
+ $(Q)mkdir -p $(DESTDIR)$(MANDIR)
+install: main.js.install style.css.install
+ @:
+
+uninstall: main.js.uninstall style.css.uninstall
+ @:
+
+clean: main.js.clean style.css.clean
+distclean: clean
+dist: dist-gz dist-xz dist-bz2
+ $(Q)rm -- $(PACKAGE)-$(VERSION)
+
+distdir:
+ $(Q)rm -rf -- $(PACKAGE)-$(VERSION)
+ $(Q)ln -s -- . $(PACKAGE)-$(VERSION)
+
+dist-gz: $(PACKAGE)-$(VERSION).tar.gz
+$(PACKAGE)-$(VERSION).tar.gz: distdir
+ @echo '[01;33m TAR > [01;37m$(PACKAGE)-$(VERSION).tar.gz[00m'
+ $(Q)tar czf $(PACKAGE)-$(VERSION).tar.gz \
+ $(PACKAGE)-$(VERSION)/client/index.ls \
+ $(PACKAGE)-$(VERSION)/client/style.sass \
+ $(PACKAGE)-$(VERSION)/client/authd.ls \
+ $(PACKAGE)-$(VERSION)/client/bulma.ls \
+ $(PACKAGE)-$(VERSION)/client/todod.ls
+
+dist-xz: $(PACKAGE)-$(VERSION).tar.xz
+$(PACKAGE)-$(VERSION).tar.xz: distdir
+ @echo '[01;33m TAR > [01;37m$(PACKAGE)-$(VERSION).tar.xz[00m'
+ $(Q)tar cJf $(PACKAGE)-$(VERSION).tar.xz \
+ $(PACKAGE)-$(VERSION)/client/index.ls \
+ $(PACKAGE)-$(VERSION)/client/style.sass \
+ $(PACKAGE)-$(VERSION)/client/authd.ls \
+ $(PACKAGE)-$(VERSION)/client/bulma.ls \
+ $(PACKAGE)-$(VERSION)/client/todod.ls
+
+dist-bz2: $(PACKAGE)-$(VERSION).tar.bz2
+$(PACKAGE)-$(VERSION).tar.bz2: distdir
+ @echo '[01;33m TAR > [01;37m$(PACKAGE)-$(VERSION).tar.bz2[00m'
+ $(Q)tar cjf $(PACKAGE)-$(VERSION).tar.bz2 \
+ $(PACKAGE)-$(VERSION)/client/index.ls \
+ $(PACKAGE)-$(VERSION)/client/style.sass \
+ $(PACKAGE)-$(VERSION)/client/authd.ls \
+ $(PACKAGE)-$(VERSION)/client/bulma.ls \
+ $(PACKAGE)-$(VERSION)/client/todod.ls
+
+help:
+ @echo '[01;37m :: todo-webclient-0.1[00m'
+ @echo ''
+ @echo '[01;37mGeneric targets:[00m'
+ @echo '[00m - [01;32mhelp [37m Prints this help message.[00m'
+ @echo '[00m - [01;32mall [37m Builds all targets.[00m'
+ @echo '[00m - [01;32mdist [37m Creates tarballs of the files of the project.[00m'
+ @echo '[00m - [01;32minstall [37m Installs the project.[00m'
+ @echo '[00m - [01;32mclean [37m Removes compiled files.[00m'
+ @echo '[00m - [01;32muninstall [37m Deinstalls the project.[00m'
+ @echo ''
+ @echo '[01;37mCLI-modifiable variables:[00m'
+ @echo ' - [01;34mPREFIX [37m ${PREFIX}[00m'
+ @echo ' - [01;34mBINDIR [37m ${BINDIR}[00m'
+ @echo ' - [01;34mLIBDIR [37m ${LIBDIR}[00m'
+ @echo ' - [01;34mSHAREDIR [37m ${SHAREDIR}[00m'
+ @echo ' - [01;34mINCLUDEDIR [37m ${INCLUDEDIR}[00m'
+ @echo ' - [01;34mMANDIR [37m ${MANDIR}[00m'
+ @echo ''
+ @echo '[01;37mProject targets: [00m'
+ @echo ' - [01;33mmain.js [37m livescript[00m'
+ @echo ' - [01;33mstyle.css [37m sass[00m'
+ @echo ''
+ @echo '[01;37mMakefile options:[00m'
+ @echo ' - gnu: false'
+ @echo ' - colors: true'
+ @echo ''
+ @echo '[01;37mRebuild the Makefile with:[00m'
+ @echo ' zsh ./build.zsh -c'
+.PHONY: all clean distclean dist install uninstall help
+
diff --git a/client/authd.ls b/client/authd.ls
new file mode 100644
index 0000000..a415ebe
--- /dev/null
+++ b/client/authd.ls
@@ -0,0 +1,84 @@
+bulma = require "./bulma.ls"
+h = require 'maquette' .h
+
+
+module.exports = {
+
+ login-widget: (model, socket) ->
+ h \div.container [
+ h \div.box [
+ if model.login-error
+ h \div.notification.is-danger [
+ model.login-error
+ ]
+ h \form [
+ bulma.field [
+ bulma.label "Login"
+ bulma.input {
+ oninput: (e) ->
+ model.login = e.target.value
+ name: \login
+ id: \login-input
+ }
+ ]
+ bulma.field [
+ bulma.label "Password"
+ bulma.input {
+ oninput: (e) ->
+ model.password = e.target.value
+ name: \password
+ type: \password
+ id: \password-input
+ }
+ ]
+
+ h \button.button.is-primary {
+ onclick: (e) ->
+ e.prevent-default!
+
+ model.login-error = undefined
+ socket.login model.login, model.password
+ } [ "Connexion" ]
+ ]
+ ]
+ ]
+
+ login-page: (model, socket) ->
+ h \div.container [
+ h \div.box [
+ if model.login-error
+ h \div.notification.is-danger [
+ model.login-error
+ ]
+ h \form [
+ bulma.field [
+ bulma.label "Login"
+ bulma.input {
+ oninput: (e) ->
+ model.login = e.target.value
+ name: \login
+ id: \login-input
+ }
+ ]
+ bulma.field [
+ bulma.label "Password"
+ bulma.input {
+ oninput: (e) ->
+ model.password = e.target.value
+ name: \password
+ type: \password
+ id: \password-input
+ }
+ ]
+
+ h \button.button.is-fullwidth.is-primary {
+ onclick: (e) ->
+ e.prevent-default!
+
+ model.login-error = undefined
+ socket.login model.login, model.password
+ } [ "Connexion" ]
+ ]
+ ]
+ ]
+}
diff --git a/client/bulma.ls b/client/bulma.ls
new file mode 100644
index 0000000..ad315a6
--- /dev/null
+++ b/client/bulma.ls
@@ -0,0 +1,39 @@
+
+h = require 'maquette' .h
+
+module.exports = {
+ box: (args, children) ->
+ h \div.box args, children
+ title: (level, args, label) ->
+ if not label
+ label = args
+ args = {}
+
+ h "div.title.is-#{level}", args, [label]
+ label: (args, label) ->
+ if not label
+ label = args
+ args = {}
+
+ h \label.label args, [label]
+ input: (args, children) ->
+ h \input.input args, children
+
+ # FIXME: Use only args and add args.label and args.input?
+ # Or maybe args.name and args.type could be used directly?
+ field: (args, children) ->
+ h \div.field args, children
+
+ modal: (args, content) ->
+ h \div.modal args, [
+ h \div.modal-background args.background
+ h \div.modal-content [args.content]
+ ]
+
+ form: (method, url, content) ->
+ h \form.form {
+ action: url
+ method: method
+ }, content
+}
+
diff --git a/client/index.ls b/client/index.ls
new file mode 100644
index 0000000..ee4c835
--- /dev/null
+++ b/client/index.ls
@@ -0,0 +1,462 @@
+
+# first:
+# connection to authd
+# display the authd widget
+#
+# second:
+# connection to todod
+# rewrite the whole body component
+#
+
+maquette = require "maquette"
+nmd = require "nano-markdown"
+authd = require "./authd.ls"
+console.log authd
+
+{create-projector, h} = maquette
+
+projector = create-projector!
+
+bulma = require "./bulma.ls"
+
+get-previous = (collection, element) ->
+ var previous
+
+ for item in collection
+ if item == element
+ return previous
+
+ previous = item
+
+get-next = (collection, element) ->
+ var found-element
+
+ for item in collection
+ if found-element
+ return item
+
+ if item == element
+ found-element := true
+
+Task = (self, project) ->
+ self.render = ->
+ author = model.users[self.author]
+ if typeof(author) != "object" and author != "request sent"
+ model.users[self.author] = "request sent"
+ # FIXME: This should go directly to authd.
+ socket.get-user self.author
+
+ assigned_to = model.users[self.assigned_to]
+ if self.assigned_to and typeof(assigned_to) != "object" and assigned_to != "request sent"
+ model.users[self.assigned_to] = "request sent"
+ # FIXME: This should go directly to authd.
+ socket.get-user self.assigned_to
+
+ is-selected = model.selected == self.id
+
+ h (\div.card.is- + (self.color || "dark")), {
+ key: self.id
+ classes: {
+ "is-selected": is-selected
+ }
+ onclick: ->
+ model.selected := self.id
+ } [
+ h \div.card-content [
+ h \div.media [
+ h \div.media-left [
+ h \img.image.is-48x48.avatar {
+ alt: "user image"
+ src: if typeof(assigned_to) == "object"
+ assigned_to.avatar
+ else
+ "https://bulma.io/images/placeholders/96x96.png"
+ }
+ ]
+ h \div.media-content [
+ if model.editing == self.id + ".title"
+ h \input.input {
+ value: self.title
+ onchange: (e) ->
+ model.editing := undefined
+ socket.edit-task project.id, self.id, {
+ title: e.target.value
+ }
+ } [ self.title ]
+ else
+ h \a [
+ bulma.title 4 self.title
+ ]
+
+ if typeof(model.users[self.assigned_to]) == "object"
+ user = model.users[self.assigned_to]
+
+ h \div.subtitle.is-6 [
+ "@" + (user.full_name || user.login)
+ ]
+ ]
+ if ! is-selected && self.description != ""
+ h \div.media-right {key: "description-icon"} [
+ h \span.icon.is-size-1 [ "π" ]
+ ]
+
+ if is-selected
+ h \div.media-right {key: "edit"} [
+ h \a.small {
+ onclick: ->
+ if model.editing == self.id + ".title"
+ model.editing := undefined
+ else
+ model.editing := self.id + ".title"
+ } [
+ "Edit"
+ ]
+ ]
+
+ if is-selected
+ h \div.media-right {key: "delete"} [
+ h \a.small {
+ onclick: ->
+ model.editing := self.id + ".delete"
+ } [
+ "Delete"
+ ]
+ ]
+ ]
+
+ if is-selected
+ h \div.content {
+ key: self.description
+ after-create: (dom) ->
+ dom.innerHTML = nmd self.description
+ } [
+ if model.editing == self.id + ".description"
+ h \form.form [
+ h \textarea.textarea {
+ value: model.editing-data
+ oninput: (e) ->
+ model.editing-data := e.target.value
+ }
+ h \div.button.is-fullwidth {
+ onclick: ->
+ socket.edit-task project.id, self.id, {
+ description: model.editing-data
+ }
+ model.editing-data := undefined
+ model.editing := undefined
+ } [ "Update" ]
+ ]
+ ]
+
+ if is-selected
+ h \span.button.is-small {
+ onclick: ->
+ model.editing-data := self.description
+ model.editing := self.id + ".description"
+ } [
+ "edit"
+ ]
+ ]
+
+ if is-selected
+ h \div.card-footer {key: "assign"} [
+ if model.editing == self.id + ".assigned_to"
+ h \div.card-footer-item {
+ key: "assign.clicked"
+ } [
+ h \input.input {
+ onchange: (e) ->
+ model.editing := undefined
+ socket.edit-task project.id, self.id, {
+ assigned_to: Number e.target.value
+ }
+ }
+ ]
+ else
+ h \a.card-footer-item {
+ key: "assign"
+ onclick: ->
+ model.editing := self.id + ".assigned_to"
+ } [ "Assign" ]
+ ]
+
+ if is-selected
+ h \div.card-footer {key: "color"} [
+ if model.editing == self.id + ".color"
+ h \div.card-footer-item {
+ key: "color.clicked"
+ } [
+ h \input.input {
+ onchange: (e) ->
+ model.editing := undefined
+ socket.edit-task project.id, self.id, {
+ color: e.target.value
+ }
+ }
+ ]
+ else
+ h \a.card-footer-item {
+ key: "assign"
+ onclick: ->
+ model.editing := self.id + ".color"
+ } [ "Change Color" ]
+ ]
+
+ if is-selected
+ h \div.card-footer {key: "move"} [
+ h \a.card-footer-item {
+ key: "β"
+ onclick: ->
+ socket.edit-task project.id, self.id, {
+ column: get-previous project.columns.map((.id)), self.column
+ }
+ } [ "β" ]
+
+ if model.editing == self.id + ".delete"
+ h \a.card-footer-item {
+ key: "delete"
+ } [
+ h \div.button.is-danger {
+ onclick: ->
+ socket.delete-task project.id, self.id
+ } [ "Delete! For real!" ]
+ ]
+
+ h \a.card-footer-item {
+ key: "β"
+ onclick: ->
+ socket.edit-task project.id, self.id, {
+ column: get-next project.columns.map((.id)), self.column
+ }
+ } [ "β" ]
+ ]
+ ]
+
+ self
+
+Project = (self) ->
+ self.tasks = self.tasks.map (e) -> Task e, self
+
+ self.render-column = (column) ->
+ h \div.column.is-3 {
+ key: column.id
+ } [
+ h \div.card.is-column-header {
+ key: column.id
+ } [
+ h \div.card-header [
+ if model.editing == column.id + ".title"
+ h \input.input {
+ type: "text",
+ value: column.name
+ onchange: (e) ->
+ console.log "onchange??"
+ model.editing := undefined
+
+ socket.edit-column self.id, column.id, {
+ name: e.target.value
+ }
+ }
+ else
+ h \div.card-header-title [
+ bulma.title 3 column.name
+ ]
+
+ h \a.card-header-icon {
+ key: "edit"
+ onclick: ->
+ if model.editing == column.id + ".title"
+ model.editing := undefined
+ else
+ model.editing := column.id + ".title"
+ } [
+ "Edit"
+ ]
+
+ if self.tasks.filter((.column == column.id)).length == 0
+ h \a.card-header-icon {
+ key: "delete"
+ onclick: ->
+ model.editing := column.id + ".delete"
+ } [
+ "Delete"
+ ]
+ ]
+ if model.editing == column.id + ".delete"
+ h \div.card-content [
+ h \div.button.is-fullwidth.is-danger {
+ onclick: ->
+ socket.delete-column self.id, column.id
+ } [ "Delete me!"]
+ ]
+ ]
+
+ for task in self.tasks
+ continue if task.column != column.id
+
+ task.render!
+
+ h \div.button.is-fullwidth {
+ onclick: ->
+ socket.new-task self.id, column.id, {
+ title: "General Kenobiβ¦"
+ description: ""
+ }
+ } [ "New task" ]
+ ]
+
+ self.render = ->
+ h \div.project {
+ key: self.id
+ } [
+ h \div.hero.is-dark { key: "title" } [
+ h \div.hero-body [
+ # FIXME: Consider using a .level for this.
+ h \div.is-pulled-right {
+ onclick: ->
+ model.editing := self.id + ".name"
+ } [
+ "Edit"
+ ]
+
+ if model.editing == self.id + ".name"
+ h \input.input {
+ onchange: (e) ->
+ model.editing := undefined
+ socket.edit-project self.id, {
+ name: e.target.value
+ }
+ value: self.name
+ }
+ else
+ h \div.title [ self.name ]
+ ]
+ ]
+
+ h \div.columns [
+ for dom in self.columns.map((column) -> self.render-column(column))
+ dom
+
+ h \div.column.is-2 {
+ key: "new-column"
+ } [
+ h \div.button.is-fullwidth {
+ onclick: ->
+ socket.new-column self.id, "Hello, there!"
+ } [ "New Column" ]
+ ]
+ ]
+ ]
+
+ self
+
+model = {
+ current-view: "login"
+ editing: undefined
+ selected: undefined
+ users: {}
+ projects: {}
+ list-todos: []
+}
+
+protocol = if location.protocol == 'https' then 'wss' else 'ws'
+port = 9999
+# socket-url = protocol + '://' + location.hostname + \: + port + "/kanban.JSON"
+socket-url = 'ws://www.junkos.netlib.re:' + port + "/kanban.JSON"
+
+console.log socket-url
+
+
+socket = todod.create-socket socket-url
+
+socket.on-close = ->
+ model.current-view := "login"
+ socket.reopen!
+
+socket.on-message = (event) ->
+ message = JSON.parse event.data
+
+ switch message.type
+ when "login"
+ model.current-view := "list-todos"
+ when "list-projects"
+ for project in message.projects
+ model.projects[project.id] = Project project
+ model.list-todos := message.projects
+ when "project"
+ model.projects[message.project.id] = Project message.project
+ when "user"
+ if message.user
+ model.users[message.user.uid] := message.user
+ when "login-error"
+ model.login-error = message.error
+ else
+ console.log "RECEIVED UNKNOWN MESSAGE TYPE: #{message.type}"
+
+ projector.schedule-render!
+
+ console.log message
+
+renderer = ->
+ render-navbar = ->
+ h \div.navbar [
+ h \div.navbar-start [
+ h \a.navbar-item.is-size-1 {
+ onclick: ->
+ model.viewed-project := undefined
+ model.current-view := "list-todos"
+ } [ "β" ]
+ ]
+ h \div.navbar-end [
+ h \a.navbar-item {
+ onclick: ->
+ model.current-view := "login"
+ socket.reopen!
+ } [ "Logout" ]
+ ]
+ authd.login-widget model, socket
+ ]
+
+ h \div.section [
+ switch model.current-view
+ when "login"
+ authd.login-page model, socket
+
+ when "list-todos"
+ h \div#list-todos [
+ render-navbar!
+
+ h \div.section (model.list-todos || []).map (project) ->
+ h \a.box {
+ key: project.id
+ onclick: ->
+ model.current-view := "project"
+ model.viewed-project := project.id
+ socket.get-project project.id
+ } [
+ bulma.title 3 project.name
+ ]
+
+ h \div.button.is-primary.is-large.is-fullwidth {
+ onclick: ->
+ socket.new-project "Hello, there!"
+ } [ "New project! (random atm)" ]
+ ]
+
+ when "project"
+ h \div [
+ render-navbar!
+
+ if model.projects[model.viewed-project]
+ model.projects[model.viewed-project].render!
+ ]
+
+ else
+ h \div.notification.is-error [
+ "Wait, what? Internal error!"
+ ]
+ ]
+
+document.add-event-listener 'DOMContentLoaded' ->
+ projector.append document.body, renderer
+
diff --git a/client/style.sass b/client/style.sass
new file mode 100644
index 0000000..5c50be7
--- /dev/null
+++ b/client/style.sass
@@ -0,0 +1,34 @@
+@charset "utf-8"
+
+@import "../node_modules/bulmaswatch/superhero/_variables.scss"
+
+// Import Bulma core
+@import "../node_modules/bulma/bulma"
+
+@import "../node_modules/bulmaswatch/superhero/_overrides.scss"
+
+.columns
+ overflow-x: scroll
+
+.avatar
+ border-radius: 4px
+
+.card.is-selected
+ border-width: 4px
+
+.is-column-header + .card, .card + .button
+ margin-top: 12px
+
+.project > .hero
+ margin-bottom: 12px
+
+.project
+ margin-top: 12px
+
+@each $name, $pair in $colors
+ $color: nth($pair, 1)
+ $color-invert: nth($pair, 2)
+
+ .card.is-#{$name}
+ background-color: darken($color, 30)
+
diff --git a/client/todod.ls b/client/todod.ls
new file mode 100644
index 0000000..2c8d585
--- /dev/null
+++ b/client/todod.ls
@@ -0,0 +1,144 @@
+# todod module
+# 1. create a socket
+# 2. the socket parses and serialize data
+# 3. the socket handles network errors
+
+module.exports = {
+
+ create-socket: (socket-url) ->
+ self = {}
+
+ self.open-socket = ->
+ console.log "Opening socket to #{socket-url}"
+ self.socket := new WebSocket socket-url
+
+ self.socket.onerror = (event) ->
+ console.log "WebSocket error.", event
+ model.state := "websocket-error"
+ self.socket.close!
+ projector.schedule-render!
+
+ self.socket.onclose = (event) ->
+ # Exporting the error in case the UI is able to deal with it.
+ model.previous-state := model.state
+ model.state := "websocket-error"
+ model.websocket-error := event.reason
+ projector.schedule-render!
+
+ self.socket.onmessage = (event) ->
+ payload = JSON.parse(event.data).payload
+
+ self.on-message {data: payload}
+
+ self.reopen = ->
+ self.socket.close!
+ self.open-socket!
+
+ self.open-socket!
+
+ self.send = (opts) ->
+ console.log JSON.stringify {
+ mtype: 0,
+ payload: opts
+ }
+
+ self.socket.send JSON.stringify {
+ mtype: 0,
+ payload: opts
+ }
+
+ self.get-project = (project-id) ->
+ self.send JSON.stringify {
+ type: "project",
+ project: project-id
+ }
+
+ self.login = (login, password) ->
+ self.send JSON.stringify {
+ type: "login",
+ login: login
+ password: password
+ }
+
+ self.get-user = (uid) ->
+ self.send JSON.stringify {
+ type: "get-user"
+ uid: uid
+ }
+
+ self.new-project = (name) ->
+ self.send JSON.stringify {
+ type: "new-project"
+ name: name
+ }
+
+ self.edit-task = (project-id, task-id, options) ->
+ payload = {
+ type: "edit-task"
+ project: project-id
+ task: task-id
+ }
+
+ for key, value of options
+ payload[key] = value
+
+ self.send JSON.stringify payload
+
+ self.delete-task = (project-id, task-id) ->
+ self.send JSON.stringify {
+ type: "delete-task"
+ project: project-id
+ task: task-id
+ }
+
+ self.edit-column = (project-id, column-id, options) ->
+ payload = {
+ type: "edit-column"
+ project: project-id
+ column: column-id
+ }
+
+ for key, value of options
+ payload[key] = value
+
+ self.send JSON.stringify payload
+
+ self.delete-column = (project-id, column-id) ->
+ self.send JSON.stringify {
+ type: "delete-column"
+ project: project-id
+ column: column-id
+ }
+
+ self.new-column = (project-id, name) ->
+ self.send JSON.stringify {
+ type: "new-column"
+ project: project-id
+ name: name
+ }
+
+ self.new-task = (project-id, column-id, options) ->
+ payload = {
+ type: "new-task"
+ project: project-id
+ column: column-id
+ }
+
+ for key, value of options
+ payload[key] = value
+
+ self.send JSON.stringify payload
+
+ self.edit-project = (project-id, options) ->
+ payload = {
+ type: "edit-project"
+ project: project-id
+ }
+
+ for key, value of options
+ payload[key] = value
+
+ self.send JSON.stringify payload
+
+ self
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..237f9ee
--- /dev/null
+++ b/index.html
@@ -0,0 +1,12 @@
+
+
+
+ Kanban
+
+
+
+
+
+
+
+
diff --git a/project.zsh b/project.zsh
new file mode 100644
index 0000000..d539963
--- /dev/null
+++ b/project.zsh
@@ -0,0 +1,12 @@
+
+package=todo-webclient
+version=0.1
+
+targets+=(main.js)
+type[main.js]=livescript
+sources[main.js]=client/index.ls
+depends[main.js]="$(ls client/*.ls | grep -v /index.ls$ | tr '\n' ' ')"
+
+targets+=(style.css)
+type[style.css]=sass
+sources[style.css]=client/style.sass