Files: cf72ae678d205ad1c930cfc58496027bd47d38b9 / app / page / calendar.js
7956 bytesRaw
1 | const nest = require('depnest') |
2 | const { h, Array: MutantArray, map, Struct, computed, watch, throttle, resolve } = require('mutant') |
3 | |
4 | const pull = require('pull-stream') |
5 | const { isMsg } = require('ssb-ref') |
6 | |
7 | exports.gives = nest({ |
8 | 'app.page.calendar': true, |
9 | 'app.html.menuItem': true |
10 | }) |
11 | |
12 | exports.needs = nest({ |
13 | 'message.html.render': 'first', |
14 | 'app.html.scroller': 'first', |
15 | 'app.sync.goTo': 'first', |
16 | 'sbot.async.get': 'first', |
17 | 'sbot.pull.stream': 'first' |
18 | }) |
19 | |
20 | exports.create = (api) => { |
21 | return nest({ |
22 | 'app.html.menuItem': menuItem, |
23 | 'app.page.calendar': calendarPage |
24 | }) |
25 | |
26 | function menuItem () { |
27 | return h('a', { |
28 | style: { order: 1 }, |
29 | 'ev-click': () => api.app.sync.goTo({ page: 'calendar' }) |
30 | }, '/calendar') |
31 | } |
32 | |
33 | function calendarPage (location) { |
34 | const d = new Date() |
35 | const state = Struct({ |
36 | today: new Date(d.getFullYear(), d.getMonth(), d.getDate()), |
37 | year: d.getFullYear(), |
38 | events: MutantArray([]), |
39 | range: Struct({ |
40 | gte: new Date(d.getFullYear(), d.getMonth(), d.getDate()), |
41 | lt: new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1) |
42 | }) |
43 | }) |
44 | |
45 | watch(state.year, year => getEvents(year, state.events, api)) |
46 | |
47 | const page = h('CalendarPage', { title: '/calendar' }, [ |
48 | Calendar(state), |
49 | Events(state, api) |
50 | ]) |
51 | |
52 | page.scroll = (i) => scroll(state.range, i) |
53 | |
54 | return page |
55 | } |
56 | } |
57 | |
58 | function scroll (range, i) { |
59 | const { gte, lt } = resolve(range) |
60 | |
61 | if (isMonthInterval(gte, lt)) { |
62 | range.gte.set(new Date(gte.getFullYear(), gte.getMonth() + i, gte.getDate())) |
63 | range.lt.set(new Date(lt.getFullYear(), lt.getMonth() + i, lt.getDate())) |
64 | return |
65 | } |
66 | |
67 | if (isWeekInterval(gte, lt)) { |
68 | range.gte.set(new Date(gte.getFullYear(), gte.getMonth(), gte.getDate() + 7 * i)) |
69 | range.lt.set(new Date(lt.getFullYear(), lt.getMonth(), lt.getDate() + 7 * i)) |
70 | return |
71 | } |
72 | |
73 | range.gte.set(new Date(gte.getFullYear(), gte.getMonth(), gte.getDate() + i)) |
74 | range.lt.set(new Date(lt.getFullYear(), lt.getMonth(), lt.getDate() + i)) |
75 | |
76 | function isMonthInterval (gte, lt) { |
77 | return gte.getDate() === 1 && // 1st of month |
78 | lt.getDate() === 1 && // to the 1st of the month |
79 | gte.getMonth() + 1 === lt.getMonth() && // one month gap |
80 | gte.getFullYear() === lt.getFullYear() |
81 | } |
82 | |
83 | function isWeekInterval (gte, lt) { |
84 | console.log( |
85 | new Date(gte.getFullYear(), gte.getMonth(), gte.getDate() + 7).toISOString() === lt.toISOString(), |
86 | new Date(gte.getFullYear(), gte.getMonth(), gte.getDate() + 7).toISOString(), |
87 | lt.toISOString(), |
88 | ) |
89 | return gte.getDay() === 1 && // from monday |
90 | lt.getDay() === 1 && // to just inside monday |
91 | new Date(gte.getFullYear(), gte.getMonth(), gte.getDate() + 7).toISOString() === lt.toISOString() |
92 | } |
93 | } |
94 | |
95 | function Events (state, api) { |
96 | return h('CalendarEvents', computed([state.events, state.range], (events, range) => { |
97 | const keys = events |
98 | .filter(ev => ev.date >= range.gte && ev.date < range.lt) |
99 | .sort((a, b) => a.date - b.date) |
100 | .map(ev => ev.data.key) |
101 | |
102 | const gatherings = MutantArray([]) |
103 | |
104 | pull( |
105 | pull.values(keys), |
106 | pull.asyncMap((key, cb) => { |
107 | api.sbot.async.get(key, (err, value) => { |
108 | if (err) return cb(err) |
109 | cb(null, {key, value}) |
110 | }) |
111 | }), |
112 | pull.drain(msg => gatherings.push(msg)) |
113 | ) |
114 | |
115 | return map(gatherings, g => api.message.html.render(g)) |
116 | })) |
117 | } |
118 | |
119 | function getEvents (year, events, api) { |
120 | const query = [{ |
121 | $filter: { |
122 | value: { |
123 | timestamp: {$gt: Number(new Date(year, 0, 1))}, // ordered by published time |
124 | content: { |
125 | type: 'about', |
126 | startDateTime: { |
127 | epoch: {$gt: 0} |
128 | } |
129 | } |
130 | } |
131 | } |
132 | }, { |
133 | $map: { |
134 | key: ['value', 'content', 'about'], // gathering |
135 | date: ['value', 'content', 'startDateTime', 'epoch'] |
136 | } |
137 | }] |
138 | |
139 | const opts = { |
140 | reverse: false, |
141 | live: true, |
142 | query |
143 | } |
144 | |
145 | pull( |
146 | api.sbot.pull.stream(server => server.query.read(opts)), |
147 | pull.filter(m => !m.sync), |
148 | pull.filter(r => isMsg(r.key) && Number.isInteger(r.date)), |
149 | pull.map(r => { |
150 | return { key: r.key, date: new Date(r.date) } |
151 | }), |
152 | pull.drain(({ key, date }) => { |
153 | var target = events.find(ev => ev.data.key === key) |
154 | if (target && target.date <= date) events.delete(target) |
155 | |
156 | events.push({ date, data: { key } }) |
157 | }) |
158 | ) |
159 | } |
160 | |
161 | // ////////////////// extract below into a module /////////////////////// |
162 | |
163 | // Thanks to nomand for the inspiration and code (https://github.com/nomand/Letnice), |
164 | // they formed the foundation of this work |
165 | |
166 | const MONTHS = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ] |
167 | const DAYS = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ] |
168 | |
169 | function Calendar (state) { |
170 | // TODO assert events is an Array of object |
171 | // of form { date, data } |
172 | |
173 | const { gte, lt } = state.range |
174 | |
175 | return h('Calendar', [ |
176 | h('div.header', [ |
177 | h('div.year', [ |
178 | state.year, |
179 | h('a', { 'ev-click': () => state.year.set(state.year() - 1) }, '-'), |
180 | h('a', { 'ev-click': () => state.year.set(state.year() + 1) }, '+') |
181 | ]) |
182 | ]), |
183 | h('div.months', computed(throttle(state, 100), ({ today, year, events, range }) => { |
184 | return MONTHS.map((month, monthIndex) => { |
185 | return Month({ month, monthIndex, today, year, events, range, gte, lt }) |
186 | }) |
187 | })) |
188 | ]) |
189 | } |
190 | |
191 | function Month ({ month, monthIndex, today, year, events, range, gte, lt }) { |
192 | const monthLength = new Date(year, monthIndex + 1, 0).getDate() |
193 | // NOTE Date takes month as a monthIndex i.e. april = 3 |
194 | // and day = 0 goes back a day |
195 | const days = Array(monthLength).fill().map((_, i) => i + 1) |
196 | |
197 | var weekday |
198 | var week |
199 | var offset = getDay(new Date(year, monthIndex, 1)) - 1 |
200 | |
201 | const setMonthRange = (ev) => { |
202 | gte.set(new Date(year, monthIndex, 1)) |
203 | lt.set(new Date(year, monthIndex + 1, 1)) |
204 | } |
205 | |
206 | return h('CalendarMonth', [ |
207 | h('div.month-name', { 'ev-click': setMonthRange }, month.substr(0, 2)), |
208 | h('div.days', { style: {display: 'grid'} }, [ |
209 | DAYS.map((day, i) => DayName(day, i)), |
210 | days.map(Day) |
211 | ]) |
212 | ]) |
213 | |
214 | function Day (day) { |
215 | const date = new Date(year, monthIndex, day) |
216 | const dateEnd = new Date(year, monthIndex, day + 1) |
217 | weekday = getDay(date) |
218 | week = Math.ceil((day + offset) / 7) |
219 | |
220 | const eventsOnDay = events.filter(e => { |
221 | return e.date >= date && e.date < dateEnd |
222 | }) |
223 | |
224 | const opts = { |
225 | attributes: { |
226 | 'title': `${year}-${monthIndex + 1}-${day}`, |
227 | 'data-date': `${year}-${monthIndex + 1}-${day}` |
228 | }, |
229 | style: { |
230 | 'grid-row': `${weekday} / ${weekday + 1}`, |
231 | 'grid-column': `${week + 1} / ${week + 2}` |
232 | // column moved by 1 to make space for labels |
233 | }, |
234 | classList: [ |
235 | date < today ? '-past' : '-future', |
236 | eventsOnDay.length ? '-events' : '', |
237 | date >= range.gte && date < range.lt ? '-selected' : '' |
238 | ], |
239 | 'ev-click': (ev) => { |
240 | if (ev.shiftKey) { |
241 | dateEnd >= resolve(lt) ? lt.set(dateEnd) : gte.set(date) |
242 | return |
243 | } |
244 | |
245 | gte.set(date) |
246 | lt.set(dateEnd) |
247 | } |
248 | } |
249 | |
250 | if (!eventsOnDay.length) return h('CalendarDay', opts) |
251 | |
252 | return h('CalendarDay', opts, [ |
253 | // TODO add awareness of whether I'm going to events |
254 | // TODO try a FontAwesome circle |
255 | h('div.dot', [ |
256 | // Math.random() > 0.3 ? h('div') : '' |
257 | ]) |
258 | ]) |
259 | } |
260 | } |
261 | |
262 | function DayName (day, index) { |
263 | return h('CalendarDayName', { |
264 | style: { |
265 | 'grid-row': `${index + 1} / ${index + 2}`, |
266 | 'grid-column': '1 / 2' |
267 | } |
268 | }, day.substr(0, 1)) |
269 | } |
270 | |
271 | function getDay (date) { |
272 | const dayIndex = date.getDay() |
273 | return dayIndex === 0 ? 7 : dayIndex |
274 | |
275 | // Weeks run 0...6 (Sun - Sat) |
276 | // this shifts those days around by 1 |
277 | } |
278 |
Built with git-ssb-web