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