git ssb

16+

Dominic / patchbay



Tree: cf72ae678d205ad1c930cfc58496027bd47d38b9

Files: cf72ae678d205ad1c930cfc58496027bd47d38b9 / app / page / calendar.js

7956 bytesRaw
1const nest = require('depnest')
2const { h, Array: MutantArray, map, Struct, computed, watch, throttle, resolve } = require('mutant')
3
4const pull = require('pull-stream')
5const { isMsg } = require('ssb-ref')
6
7exports.gives = nest({
8 'app.page.calendar': true,
9 'app.html.menuItem': true
10})
11
12exports.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
20exports.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
58function 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
95function 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
119function 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
166const MONTHS = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]
167const DAYS = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ]
168
169function 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
191function 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
262function 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
271function 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