commit
8b5e0a0c02
@ -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
|
||||
}
|
||||
|
@ -0,0 +1,485 @@
|
||||
|
||||
maquette = require "maquette"
|
||||
nmd = require "nano-markdown"
|
||||
|
||||
{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.send JSON.stringify {
|
||||
type: "get-user"
|
||||
uid: self.author
|
||||
}
|
||||
|
||||
assigned_to = model.users[self.assigned_to]
|
||||
if 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.send JSON.stringify {
|
||||
type: "get-user"
|
||||
uid: self.assigned_to
|
||||
}
|
||||
|
||||
is-selected = model.selected == self.id
|
||||
|
||||
h \div.card {
|
||||
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.send JSON.stringify {
|
||||
type: "edit-task"
|
||||
project: project.id
|
||||
task: self.id
|
||||
title: e.target.value
|
||||
}
|
||||
} [ self.title ]
|
||||
else
|
||||
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
|
||||
h \div.media-right {key: "edit"} [
|
||||
h \span.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 \span.small {
|
||||
onclick: ->
|
||||
model.editing := self.id + ".delete"
|
||||
} [
|
||||
"Delete"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
if is-selected
|
||||
h \div.content {
|
||||
after-create: (dom) ->
|
||||
dom.innerHTML = nmd self.description
|
||||
} [
|
||||
if model.editing == self.id + ".description"
|
||||
h \form.form [
|
||||
h \textarea.textarea {
|
||||
value: self.description
|
||||
oninput: (e) ->
|
||||
model.editing-data := e.target.value
|
||||
}
|
||||
h \div.button.is-fullwidth {
|
||||
onclick: ->
|
||||
socket.send JSON.stringify {
|
||||
type: "edit-task"
|
||||
project: project.id
|
||||
task: 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 := 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.send JSON.stringify {
|
||||
type: "edit-task"
|
||||
project: project.id
|
||||
task: self.id
|
||||
assigned_to: Number e.target.value
|
||||
}
|
||||
}
|
||||
]
|
||||
else
|
||||
h \div.card-footer-item {
|
||||
key: "assign"
|
||||
onclick: ->
|
||||
model.editing := self.id + ".assigned_to"
|
||||
} [ "Assign" ]
|
||||
]
|
||||
|
||||
if is-selected
|
||||
h \div.card-footer {key: "move"} [
|
||||
h \div.card-footer-item {
|
||||
key: "⇐"
|
||||
onclick: ->
|
||||
socket.send JSON.stringify {
|
||||
type: "edit-task"
|
||||
project: project.id
|
||||
task: self.id
|
||||
column: get-previous project.columns.map((.id)), self.column
|
||||
}
|
||||
} [ "⇐" ]
|
||||
|
||||
if model.editing == self.id + ".delete"
|
||||
h \div.card-footer-item {
|
||||
key: "delete"
|
||||
} [
|
||||
h \div.button.is-danger {
|
||||
onclick: ->
|
||||
socket.send JSON.stringify {
|
||||
type: "delete-task"
|
||||
project: project.id
|
||||
task: self.id
|
||||
}
|
||||
} [ "Delete! For real!" ]
|
||||
]
|
||||
|
||||
h \div.card-footer-item {
|
||||
key: "⇒"
|
||||
onclick: ->
|
||||
socket.send JSON.stringify {
|
||||
type: "edit-task"
|
||||
project: project.id
|
||||
task: 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-2 {
|
||||
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.send JSON.stringify {
|
||||
type: "edit-column"
|
||||
project: self.id
|
||||
column: column.id
|
||||
name: e.target.value
|
||||
}
|
||||
}
|
||||
else
|
||||
h \div.card-header-title [
|
||||
bulma.title 3 column.name
|
||||
]
|
||||
|
||||
h \div.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 \div.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.send JSON.stringify {
|
||||
type: "delete-column"
|
||||
project: self.id
|
||||
column: column.id
|
||||
}
|
||||
} [ "Delete me!"]
|
||||
]
|
||||
]
|
||||
|
||||
for task in self.tasks
|
||||
continue if task.column != column.id
|
||||
|
||||
task.render!
|
||||
|
||||
h \div.button.is-fullwidth {
|
||||
onclick: ->
|
||||
socket.send JSON.stringify {
|
||||
type: "new-task"
|
||||
project: self.id
|
||||
column: 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.send JSON.stringify {
|
||||
type: "edit-project"
|
||||
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.send JSON.stringify {
|
||||
type: "new-column"
|
||||
project: self.id
|
||||
name: "Hello, there!"
|
||||
}
|
||||
} [ "New Column" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
self
|
||||
|
||||
model = {
|
||||
current-view: "login"
|
||||
editing: undefined
|
||||
selected: undefined
|
||||
users: {}
|
||||
projects: {}
|
||||
projects-list: []
|
||||
}
|
||||
|
||||
socket = new WebSocket "ws://localhost:8888/socket"
|
||||
|
||||
socket.onopen = (event) ->
|
||||
# Nothing to do here ATM.
|
||||
|
||||
socket.onerror = (event) ->
|
||||
model.state = "websocket-error"
|
||||
projector.schedule-render!
|
||||
|
||||
socket.onclase = (event) ->
|
||||
model.state = "websocket-error"
|
||||
model.websocket-error = event.reason
|
||||
projector.schedule-render!
|
||||
|
||||
socket.onmessage = (event) ->
|
||||
console.log event.data
|
||||
|
||||
message = JSON.parse event.data
|
||||
|
||||
switch message.type
|
||||
when "login"
|
||||
model.current-view := "projects-list"
|
||||
when "list-projects"
|
||||
for project in message.projects
|
||||
model.projects[project.id] = Project project
|
||||
model.projects-list := message.projects
|
||||
when "project"
|
||||
model.projects[message.project.id] = Project message.project
|
||||
when "user"
|
||||
model.users[message.user.uid] := message.user
|
||||
else
|
||||
console.log "RECEIVED UNKNOWN MESSAGE TYPE: #{message.type}"
|
||||
|
||||
projector.schedule-render!
|
||||
|
||||
console.log message
|
||||
|
||||
renderer = ->
|
||||
h \div.section [
|
||||
switch model.current-view
|
||||
when "login"
|
||||
h \div.container [
|
||||
h \div.box [
|
||||
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!
|
||||
|
||||
socket.send JSON.stringify {
|
||||
type: "login",
|
||||
login: model.login
|
||||
password: model.password
|
||||
}
|
||||
} [ "Letsu go!" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
when "project"
|
||||
h \div [
|
||||
h \div.navbar [
|
||||
h \div.navbar-end [
|
||||
h \a.navbar-item {
|
||||
onclick: ->
|
||||
model.viewed-project := undefined
|
||||
model.current-view := "projects-list"
|
||||
} [ "Go back" ]
|
||||
]
|
||||
]
|
||||
|
||||
if model.projects[model.viewed-project]
|
||||
model.projects[model.viewed-project].render!
|
||||
]
|
||||
when "projects-list"
|
||||
h \div#projects-list [
|
||||
for project in (model.projects-list || [])
|
||||
h \div.box {
|
||||
key: project.id
|
||||
onclick: ->
|
||||
model.current-view := "project"
|
||||
model.viewed-project := project.id
|
||||
socket.send JSON.stringify {
|
||||
type: "project",
|
||||
project: project.id
|
||||
}
|
||||
} [
|
||||
bulma.title 3 project.name
|
||||
]
|
||||
|
||||
h \div.button.is-primary.is-large.is-fullwidth {
|
||||
onclick: ->
|
||||
socket.send JSON.stringify {
|
||||
type: "new-project"
|
||||
name: "Hello, there!"
|
||||
}
|
||||
} [ "New project! (random atm)" ]
|
||||
]
|
||||
else
|
||||
h \div.notification.is-error [
|
||||
"Wait, what? Internal error!"
|
||||
]
|
||||
]
|
||||
|
||||
document.add-event-listener 'DOMContentLoaded' ->
|
||||
projector.append document.body, renderer
|
||||
|
@ -0,0 +1,27 @@
|
||||
@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
|
||||
|
@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Kanban</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="stylesheet" href="public/style.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<script src="public/main.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "kanban",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"babel": "^6.23.0",
|
||||
"babel-cli": "^6.26.0",
|
||||
"browserify": "^16.2.3",
|
||||
"browserify-livescript": "^0.2.4",
|
||||
"bulma": "^0.7.5",
|
||||
"bulmaswatch": "^0.7.2",
|
||||
"livescript": "^1.6.0",
|
||||
"maquette": "^3.3.4",
|
||||
"nano-markdown": "^1.2.0"
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
name: kanban
|
||||
version: 0.1.0
|
||||
|
||||
# authors:
|
||||
# - name <email@example.com>
|
||||
|
||||
# description: |
|
||||
# Short description of kanban
|
||||
|
||||
dependencies:
|
||||
kemal:
|
||||
github: kemalcr/kemal
|
||||
authd:
|
||||
github: Lukc/authd
|
||||
fs:
|
||||
git: https://git.karchnu.fr/JunkOS/fs.cr
|
||||
|
||||
# pg:
|
||||
# github: will/crystal-pg
|
||||
# version: "~> 0.5"
|
||||
|
||||
# development_dependencies:
|
||||
# webmock:
|
||||
# github: manastech/webmock.cr
|
||||
|
||||
# license: MIT
|
Loading…
Reference in new issue