git ssb

16+

Dominic / patchbay



Tree: f7fe8b6d565d3823c3d5cf96317c609472e7cdbf

Files: f7fe8b6d565d3823c3d5cf96317c609472e7cdbf / app / page / calendar.js

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

Built with git-ssb-web