Files: fdb2ee6f55290529cd2d95134844044dd149a47c / views / show.js
10842 bytesRaw
1 | const { h, Struct, computed, resolve } = require('mutant') |
2 | const pull = require('pull-stream') |
3 | const printTime = require('../lib/print-time') |
4 | |
5 | module.exports = function ScryShow (opts) { |
6 | const { |
7 | poll, |
8 | myFeedId, |
9 | scuttle, |
10 | name = k => k.slice(0, 9), |
11 | avatar = k => h('img'), |
12 | testing = false |
13 | } = opts |
14 | |
15 | const state = Struct(initialState()) |
16 | fetchState() |
17 | watchForUpdates(fetchState) |
18 | |
19 | return h('ScryShow', [ |
20 | h('h1', state.current.title), |
21 | ScryShowClosesAt(state.current), |
22 | AuthorActions(), |
23 | ScryShowResults(), |
24 | h('div.actions', [ |
25 | PublishBtn() |
26 | ]) |
27 | ]) |
28 | |
29 | function AuthorActions () { |
30 | if (poll.value.author !== myFeedId) return |
31 | |
32 | // TODO hide if already resolved ? |
33 | |
34 | return h('div.author-actions', [ |
35 | ResolveBtn(), |
36 | PublishResolveBtn() |
37 | ]) |
38 | |
39 | function ResolveBtn () { |
40 | const toggleResolving = () => { |
41 | const newState = !resolve(state.mode.isResolving) |
42 | state.mode.isResolving.set(newState) |
43 | } |
44 | |
45 | return h('button', { 'ev-click': toggleResolving }, 'Resolve') |
46 | } |
47 | |
48 | function PublishResolveBtn () { |
49 | const publish = () => { |
50 | const choices = resolve(state.next.resolution) |
51 | .reduce((acc, choice, i) => { |
52 | if (choice) acc.push(i) |
53 | return acc |
54 | }, []) |
55 | // const mentions = [] |
56 | scuttle.poll.async.publishResolution({ |
57 | poll: poll, |
58 | choices |
59 | }, (err, data) => console.log('resolution:', err, data)) |
60 | } |
61 | return h('button', { 'ev-click': publish }, 'Publish Resolution') |
62 | } |
63 | } |
64 | |
65 | function PublishBtn () { |
66 | const publish = () => { |
67 | state.mode.isPublishing.set(true) |
68 | const choices = resolve(state.next.position).reduce((acc, el, i) => { |
69 | if (el) acc.push(i) |
70 | return acc |
71 | }, []) |
72 | |
73 | scuttle.position.async.publishMeetingTime({ poll, choices }, (err, data) => { |
74 | if (err) throw err |
75 | console.log(data) |
76 | }) |
77 | } |
78 | |
79 | return computed([state.current, state.next, state.mode], (current, next, mode) => { |
80 | if (validResolution(current.resolution)) return |
81 | if (!mode.isEditing) return |
82 | if (mode.isPublishing) return h('button', h('i.fa.fa-spin.fa-pulse')) |
83 | if (mode.isResolving) return |
84 | |
85 | const isNewPosition = current.position.join() !== next.position.join() |
86 | return h('button', |
87 | { |
88 | className: isNewPosition ? '-primary' : '', |
89 | disabled: !isNewPosition, |
90 | 'ev-click': publish |
91 | }, 'Publish' |
92 | ) |
93 | }) |
94 | } |
95 | |
96 | function ScryShowResults () { |
97 | return computed([state.current, state.next.resolution, state.mode], (current, nextResolution, { isResolving }) => { |
98 | const { times, rows, resolution } = current |
99 | const style = { |
100 | display: 'grid', |
101 | 'grid-template-columns': `minmax(10rem, auto) repeat(${times.length}, 4rem)` |
102 | } |
103 | |
104 | const getChosenClass = i => { |
105 | const relevant = isResolving ? nextResolution : resolution |
106 | if (!validResolution(relevant)) return '' |
107 | return relevant[i] ? '-chosen' : '-not-chosen' |
108 | } |
109 | |
110 | return [ |
111 | h('ScryShowResults', { style }, [ |
112 | ScryShowTimes(times, getChosenClass), |
113 | ScryShowResolution(times, resolution), |
114 | ScryShowSummary(rows, getChosenClass), |
115 | ScryShowPositions(rows) |
116 | ]) |
117 | ] |
118 | }) |
119 | } |
120 | |
121 | function ScryShowPositions (rows) { |
122 | return rows.map(({ author, position }) => { |
123 | if (author !== myFeedId) return OtherPosition(author, position) |
124 | else return MyPosition(position) |
125 | }) |
126 | |
127 | function OtherPosition (author, position) { |
128 | return [ |
129 | h('div.about', [ |
130 | avatar(author), |
131 | name(author) |
132 | ]), |
133 | position.map(pos => pos |
134 | ? h('div.position.-yes', tick()) |
135 | : h('div.position.-no') |
136 | ) |
137 | ] |
138 | } |
139 | |
140 | function MyPosition (position) { |
141 | const toggleEditing = () => { |
142 | const newState = !resolve(state.mode.isEditing) |
143 | state.mode.isEditing.set(newState) |
144 | } |
145 | |
146 | // TODO disable pencil with resolution exists |
147 | return [ |
148 | h('div.about', [ |
149 | avatar(myFeedId), |
150 | name(myFeedId), |
151 | h('i.fa.fa-pencil', { 'ev-click': toggleEditing }) |
152 | ]), |
153 | computed([state.current.position, state.next.position, state.mode.isEditing], (position, nextPosition, isEditing) => { |
154 | if (!isEditing) { |
155 | return position.map(pos => pos |
156 | ? h('div.position.-yes', tick()) |
157 | : h('div.position.-no') |
158 | ) |
159 | } |
160 | |
161 | return nextPosition.map((pos, i) => { |
162 | return h('div.position.-edit', |
163 | { |
164 | 'ev-click': () => { |
165 | const newState = resolve(nextPosition) |
166 | newState[i] = !pos |
167 | state.next.position.set(newState) |
168 | } |
169 | }, |
170 | pos ? checkedBox() : uncheckedBox() |
171 | ) |
172 | }) |
173 | }) |
174 | ] |
175 | } |
176 | } |
177 | |
178 | function ScryShowResolution (times, resolution) { |
179 | return computed([state.mode.isResolving, state.next.resolution], (isResolving, nextResolution) => { |
180 | if (!isResolving && validResolution(resolution)) { |
181 | return times.map((_, i) => { |
182 | const style = { 'grid-column': i + 2 } // grid-columns start at 1 D: |
183 | const isChoice = Boolean(resolution[i]) |
184 | const className = isChoice ? '-chosen' : '' |
185 | |
186 | return h('div.resolution', { style, className }, |
187 | isChoice ? star() : '' |
188 | ) |
189 | }) |
190 | } |
191 | |
192 | if (isResolving) { |
193 | const toggleChoice = (i) => { |
194 | const newState = Array.from(nextResolution) |
195 | newState[i] = !nextResolution[i] |
196 | state.next.resolution.set(newState) |
197 | } |
198 | return [ |
199 | h('div.resolve-label', 'Final options'), |
200 | times.map((_, i) => { |
201 | const isChoice = Boolean(nextResolution[i]) |
202 | const classList = [ '-highlighted', isChoice ? '-chosen' : '' ] |
203 | |
204 | return h('div.resolution', |
205 | { classList, 'ev-click': () => toggleChoice(i) }, |
206 | isChoice ? star() : starEmpty() |
207 | ) |
208 | }) |
209 | ] |
210 | } |
211 | }) |
212 | } |
213 | |
214 | function ScryShowSummary (rows, getChosenClass) { |
215 | if (!rows.length) return |
216 | |
217 | const participants = rows.filter(r => r.position[0] !== null).length |
218 | |
219 | const counts = rows[0].position.map((_, i) => { |
220 | return rows.reduce((acc, row) => { |
221 | if (row.position[i] === true) acc += 1 |
222 | return acc |
223 | }, 0) |
224 | }) |
225 | return [ |
226 | h('div.participants', participants === 1 |
227 | ? `${participants} participant` |
228 | : `${participants} participants` |
229 | ), |
230 | counts.map((n, i) => { |
231 | return h('div.count', { className: getChosenClass(i) }, |
232 | `${n}${tick()}` |
233 | ) |
234 | }) |
235 | ] |
236 | } |
237 | |
238 | function fetchState () { |
239 | scuttle.poll.async.get(poll.key, (err, doc) => { |
240 | if (err) return console.error(err) |
241 | |
242 | const { title, closesAt, positions } = doc |
243 | const times = doc.results.map(result => result.choice) |
244 | const results = times.map(t => doc.results.find(result => result.choice === t)) |
245 | // this ensures results Array is in same order as a times Array |
246 | |
247 | const rows = positions |
248 | .reduce((acc, pos) => { |
249 | if (!acc.includes(pos.value.author)) acc.push(pos.value.author) |
250 | return acc |
251 | }, []) |
252 | .map(author => { |
253 | const position = times.map((time, i) => { |
254 | return results[i].voters.hasOwnProperty(author) |
255 | }) |
256 | return { author, position } |
257 | }) |
258 | |
259 | const myRow = rows.find(r => r.author === myFeedId) |
260 | const myPosition = myRow ? myRow.position : Array(times.length).fill(null) |
261 | |
262 | var resolution = Array(times.length).fill(null) |
263 | if (doc.resolution) { |
264 | resolution = resolution.map((_, i) => doc.resolution.choices.includes(i)) |
265 | } |
266 | var nextResolution = resolution.map(el => el || false) |
267 | |
268 | var isEditing = false |
269 | if (!myRow && !validResolution(resolution)) { |
270 | rows.push({ author: myFeedId, position: myPosition }) |
271 | isEditing = true |
272 | } |
273 | |
274 | state.current.set({ |
275 | title, |
276 | closesAt, |
277 | times, |
278 | rows, |
279 | position: myPosition, |
280 | resolution |
281 | }) |
282 | state.next.set({ |
283 | position: Array.from(myPosition), |
284 | resolution: nextResolution |
285 | }) |
286 | state.mode.set({ |
287 | isEditing, |
288 | isPublishing: false, |
289 | isResolving: false |
290 | }) |
291 | }) |
292 | } |
293 | |
294 | function watchForUpdates (cb) { |
295 | // TODO check if isEditing before calling cb |
296 | // start a loop to trigger cb after finished editing |
297 | pull( |
298 | scuttle.poll.pull.updates(poll.key), |
299 | pull.filter(m => !m.sync), |
300 | pull.drain(m => { |
301 | cb() |
302 | }) |
303 | ) |
304 | } |
305 | |
306 | function tick () { return '✔' } |
307 | function checkedBox () { return testing ? '☑' : h('i.fa.fa-check-square-o') } |
308 | function uncheckedBox () { return testing ? '☐' : h('i.fa.fa-square-o') } |
309 | function star () { return testing ? '★' : h('i.fa.fa-star') } |
310 | function starEmpty () { return testing ? '☐' : h('i.fa.fa-star-o') } |
311 | } |
312 | |
313 | function initialState () { |
314 | return { |
315 | current: Struct({ |
316 | title: '', |
317 | times: [], |
318 | closesAt: undefined, |
319 | rows: [], |
320 | position: [], |
321 | resolution: [] |
322 | }), |
323 | next: Struct({ |
324 | position: [], |
325 | resolution: [] |
326 | }), |
327 | mode: Struct({ |
328 | isEditing: false, |
329 | isPublishing: false, |
330 | isResolving: false |
331 | }) |
332 | } |
333 | } |
334 | |
335 | function validResolution (arr) { |
336 | // valid as in not a dummy resolution that's a placeholder |
337 | return arr.every(el => el !== null) |
338 | } |
339 | |
340 | // component |
341 | |
342 | function ScryShowClosesAt ({ closesAt, resolution }) { |
343 | return h('div.closes-at', computed([closesAt, resolution], (t, resolution) => { |
344 | if (!t) return |
345 | if (validResolution(resolution)) return |
346 | |
347 | const distance = t - new Date() |
348 | if (distance < 0) return 'This scry has closed, but a resolution has yet to be declared.' |
349 | |
350 | const hours = Math.floor(distance / (60 * 60e3)) |
351 | const days = Math.floor(hours / 24) |
352 | return `This scry closes in ${days} days, ${hours % 24} hours` |
353 | })) |
354 | } |
355 | |
356 | // component: show-time |
357 | |
358 | function ScryShowTimes (times, getChosenClass) { |
359 | return times.map((time, i) => { |
360 | const style = { 'grid-column': i + 2 } // grid-columns start at 1 D: |
361 | |
362 | return h('ScryShowTime', { style, className: getChosenClass(i) }, [ |
363 | h('div.month', month(time)), |
364 | h('div.date', time.getDate()), |
365 | h('div.day', day(time)), |
366 | h('div.time', printTime(time)) |
367 | ]) |
368 | }) |
369 | } |
370 | |
371 | function month (date) { |
372 | const months = ['Jan', 'Feb', 'March', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'] |
373 | |
374 | return months[date.getMonth()] |
375 | } |
376 | |
377 | function day (date) { |
378 | const days = ['Sun', 'Mon', 'Tues', 'Wed', 'Thu', 'Fri', 'Sat'] |
379 | |
380 | return days[date.getDay()] |
381 | } |
382 |
Built with git-ssb-web