Files: 83748629af52473497dd0896a5c487f66982c8ff / views / show.js
8113 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.now.title), |
21 | ScryShowClosesAt(state.now), |
22 | ScryShowResults(), |
23 | h('div.actions', [ |
24 | PublishBtn() |
25 | ]) |
26 | ]) |
27 | |
28 | function PublishBtn () { |
29 | return computed([state.now, state.next], (current, next) => { |
30 | if (current.resolution) return |
31 | if (!next.isEditing) return |
32 | if (next.isPublishing) return h('button', h('i.fa.fa-spin.fa-pulse')) |
33 | |
34 | const newPosition = current.position.join() !== next.position.join() |
35 | return h('button', |
36 | { |
37 | className: newPosition ? '-primary' : '', |
38 | disabled: !newPosition, |
39 | 'ev-click': () => { |
40 | state.next.isPublishing.set(true) |
41 | const choices = next.position.reduce((acc, el, i) => { |
42 | if (el) acc.push(i) |
43 | return acc |
44 | }, []) |
45 | |
46 | scuttle.position.async.publishMeetingTime({ poll, choices }, (err, data) => { |
47 | if (err) throw err |
48 | console.log(data) |
49 | }) |
50 | } |
51 | }, 'Publish' |
52 | ) |
53 | }) |
54 | } |
55 | |
56 | function ScryShowResults () { |
57 | return computed(state.now, ({ title, closesAt, times, rows, resolution }) => { |
58 | const style = { |
59 | display: 'grid', |
60 | 'grid-template-columns': `minmax(10rem, auto) repeat(${times.length}, 4rem)` |
61 | } |
62 | |
63 | const getChosenClass = i => { |
64 | if (!resolution) return '' |
65 | return resolution.choices.includes(i) ? '-chosen' : '-not-chosen' |
66 | } |
67 | |
68 | return [ |
69 | h('ScryShowResults', { style }, [ |
70 | ScryShowTimes(times, getChosenClass), |
71 | ScryShowResolution(times, resolution), |
72 | ScryShowSummary(rows, getChosenClass), |
73 | ScryShowPositions(rows) |
74 | ]) |
75 | ] |
76 | }) |
77 | } |
78 | |
79 | function ScryShowPositions (rows) { |
80 | return rows.map(({ author, position }) => { |
81 | if (author !== myFeedId) return OtherPosition(author, position) |
82 | else return MyPosition(position) |
83 | }) |
84 | |
85 | function OtherPosition (author, position) { |
86 | return [ |
87 | h('div.about', [ |
88 | avatar(author), |
89 | name(author) |
90 | ]), |
91 | position.map(pos => pos |
92 | ? h('div.position.-yes', tick()) |
93 | : h('div.position.-no') |
94 | ) |
95 | ] |
96 | } |
97 | |
98 | function MyPosition (position) { |
99 | const toggleEditing = () => { |
100 | const isEditing = !resolve(state.next.isEditing) |
101 | state.next.isEditing.set(isEditing) |
102 | } |
103 | |
104 | return [ |
105 | h('div.about', [ |
106 | avatar(myFeedId), |
107 | name(myFeedId), |
108 | h('i.fa.fa-pencil', { 'ev-click': toggleEditing }) |
109 | ]), |
110 | computed([state.next, state.now.position], ({ isEditing, position }, currentPosition) => { |
111 | if (!isEditing) { |
112 | return currentPosition.map(pos => pos |
113 | ? h('div.position.-yes', tick()) |
114 | : h('div.position.-no') |
115 | ) |
116 | } |
117 | |
118 | return position.map((pos, i) => { |
119 | return h('div.position.-edit', |
120 | { |
121 | 'ev-click': () => { |
122 | const nextPosition = resolve(state.next.position) |
123 | nextPosition[i] = !pos |
124 | state.next.position.set(nextPosition) |
125 | } |
126 | }, |
127 | pos ? checkedBox() : uncheckedBox() |
128 | ) |
129 | }) |
130 | }) |
131 | ] |
132 | } |
133 | } |
134 | |
135 | function ScryShowResolution (times, resolution) { |
136 | if (!resolution) return |
137 | |
138 | return times.map((_, i) => { |
139 | const style = { 'grid-column': i + 2 } // grid-columns start at 1 D: |
140 | const isChoice = resolution.choices.includes(i) |
141 | const className = isChoice ? '-chosen' : '' |
142 | |
143 | return h('div.resolution', { style, className }, |
144 | isChoice ? star() : '' |
145 | ) |
146 | }) |
147 | } |
148 | |
149 | function ScryShowSummary (rows, getChosenClass) { |
150 | if (!rows.length) return |
151 | |
152 | const participants = rows.filter(r => r.position[0] !== null).length |
153 | |
154 | const counts = rows[0].position.map((_, i) => { |
155 | return rows.reduce((acc, row) => { |
156 | if (row.position[i] === true) acc += 1 |
157 | return acc |
158 | }, 0) |
159 | }) |
160 | return [ |
161 | h('div.participants', participants === 1 |
162 | ? `${participants} participant` |
163 | : `${participants} participants` |
164 | ), |
165 | counts.map((n, i) => { |
166 | return h('div.count', { className: getChosenClass(i) }, |
167 | `${n}${tick()}` |
168 | ) |
169 | }) |
170 | ] |
171 | } |
172 | |
173 | function fetchState () { |
174 | scuttle.poll.async.get(poll.key, (err, doc) => { |
175 | if (err) return console.error(err) |
176 | |
177 | const { title, closesAt, positions } = doc |
178 | const times = doc.results.map(result => result.choice) |
179 | const results = times.map(t => doc.results.find(result => result.choice === t)) |
180 | // this ensures results Array is in same order as a times Array |
181 | |
182 | const rows = positions |
183 | .reduce((acc, pos) => { |
184 | if (!acc.includes(pos.value.author)) acc.push(pos.value.author) |
185 | return acc |
186 | }, []) |
187 | .map(author => { |
188 | const position = times.map((time, i) => { |
189 | return results[i].voters.hasOwnProperty(author) |
190 | }) |
191 | return { author, position } |
192 | }) |
193 | |
194 | const myRow = rows.find(r => r.author === myFeedId) |
195 | const myPosition = myRow ? myRow.position : Array(times.length).fill(null) |
196 | |
197 | var isEditing = false |
198 | if (!myRow && !doc.resolution) { |
199 | rows.push({ author: myFeedId, position: myPosition }) |
200 | isEditing = true |
201 | } |
202 | |
203 | state.now.set({ |
204 | title, |
205 | closesAt, |
206 | times, |
207 | rows, |
208 | resolution: doc.resolution, |
209 | position: myPosition |
210 | }) |
211 | state.next.set({ |
212 | position: Array.from(myPosition), |
213 | isEditing, |
214 | isPublishing: false |
215 | }) |
216 | }) |
217 | } |
218 | |
219 | function watchForUpdates (cb) { |
220 | // TODO check if isEditing before calling cb |
221 | // start a loop to trigger cb after finished editing |
222 | pull( |
223 | scuttle.poll.pull.updates(poll.key), |
224 | pull.filter(m => !m.sync), |
225 | pull.drain(m => { |
226 | cb() |
227 | }) |
228 | ) |
229 | } |
230 | |
231 | function tick () { return '✔' } |
232 | function checkedBox () { return testing ? '☑' : h('i.fa.fa-check-square-o') } |
233 | function uncheckedBox () { return testing ? '☐' : h('i.fa.fa-square-o') } |
234 | function star () { return testing ? '★' : h('i.fa.fa-star') } |
235 | } |
236 | |
237 | function initialState () { |
238 | return { |
239 | now: Struct({ |
240 | title: '', |
241 | times: [], |
242 | closesAt: undefined, |
243 | resolution: undefined, |
244 | rows: [], |
245 | position: [] |
246 | }), |
247 | next: Struct({ |
248 | position: [], |
249 | isEditing: false, |
250 | isPublishing: false |
251 | }) |
252 | } |
253 | } |
254 | |
255 | function ScryShowClosesAt ({ closesAt, resolution }) { |
256 | return h('div.closes-at', computed([closesAt, resolution], (t, resolution) => { |
257 | if (resolution) return |
258 | if (!t) return |
259 | |
260 | const distance = t - new Date() |
261 | if (distance < 0) return 'This scry has closed, but a resolution has yet to be declared.' |
262 | |
263 | const hours = Math.floor(distance / (60 * 60e3)) |
264 | const days = Math.floor(hours / 24) |
265 | return `This scry closes in ${days} days, ${hours % 24} hours` |
266 | })) |
267 | } |
268 | |
269 | // component: show-time |
270 | |
271 | function ScryShowTimes (times, getChosenClass) { |
272 | return times.map((time, i) => { |
273 | const style = { 'grid-column': i + 2 } // grid-columns start at 1 D: |
274 | |
275 | return h('ScryShowTime', { style, className: getChosenClass(i) }, [ |
276 | h('div.month', month(time)), |
277 | h('div.date', time.getDate()), |
278 | h('div.day', day(time)), |
279 | h('div.time', printTime(time)) |
280 | ]) |
281 | }) |
282 | } |
283 | |
284 | function month (date) { |
285 | const months = ['Jan', 'Feb', 'March', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'] |
286 | |
287 | return months[date.getMonth()] |
288 | } |
289 | |
290 | function day (date) { |
291 | const days = ['Sun', 'Mon', 'Tues', 'Wed', 'Thu', 'Fri', 'Sat'] |
292 | |
293 | return days[date.getDay()] |
294 | } |
295 |
Built with git-ssb-web