git ssb

16+

Dominic / patchbay



Tree: ca8ac8d21e58b7d37981f6ee097ce54b1bf10b22

Files: ca8ac8d21e58b7d37981f6ee097ce54b1bf10b22 / app / page / calendar.js

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

Built with git-ssb-web