1
+ import React from 'react'
2
+ import firebase from 'firebase/app'
3
+ import 'firebase/auth'
4
+ import 'firebase/database'
5
+ import firebaseui from 'firebaseui'
6
+ const { HomebaseProvider, useClient, useTransact, useQuery, useEntity } = window . homebase . react
7
+
8
+ export const App = ( ) => {
9
+ return (
10
+ < HomebaseProvider config = { config } >
11
+ < AuthPrompt >
12
+ < Todos />
13
+ </ AuthPrompt >
14
+ </ HomebaseProvider >
15
+ )
16
+ }
17
+
18
+ const config = {
19
+ // Schema is only used to enforce
20
+ // unique constraints and relationships.
21
+ // It is not a type system, yet.
22
+ schema : {
23
+ user : { uid : { unique : 'identity' } } ,
24
+ todo : {
25
+ // refs are relationships
26
+ project : { type : 'ref' } ,
27
+ owner : { type : 'ref' }
28
+ }
29
+ } ,
30
+ // Initial data let's you conveniently transact some
31
+ // starting data on DB creation to hydrate your components.
32
+ initialData : [
33
+ {
34
+ todoFilter : {
35
+ // identity is a special unique attribute for user generated ids
36
+ // E.g. todoFilters are settings that should be easy to lookup by their identity
37
+ identity : 'todoFilters' ,
38
+ showCompleted : true
39
+ }
40
+ } , {
41
+ user : {
42
+ // Negative numbers can be used as temporary ids in a transaction.
43
+ // Use them to relate multiple entities together at once.
44
+ id : - 1 ,
45
+ name : 'Stella'
46
+ }
47
+ } , {
48
+ user : {
49
+ id : - 2 ,
50
+ name : 'Arpegius'
51
+ }
52
+ } , {
53
+ project : {
54
+ id : - 3 ,
55
+ name : 'Make it'
56
+ }
57
+ } , {
58
+ project : {
59
+ id : - 4 ,
60
+ name : 'Do it'
61
+ }
62
+ }
63
+ ]
64
+ }
65
+
66
+ const firebaseConfig = {
67
+ apiKey : "AIzaSyC31X8R5-doWtVmbBRD0xCue09HfydfjzI" ,
68
+ authDomain : "homebase-react.firebaseapp.com" ,
69
+ databaseURL : "https://homebase-react.firebaseio.com" ,
70
+ projectId : "homebase-react" ,
71
+ storageBucket : "homebase-react.appspot.com" ,
72
+ messagingSenderId : "1056367825432" ,
73
+ appId : "1:1056367825432:web:a6aaba7bee5e8a43e6296d" ,
74
+ measurementId : "G-FJ9BNZDFCE"
75
+ }
76
+
77
+ firebase . initializeApp ( firebaseConfig )
78
+ const firebaseUI = new firebaseui . auth . AuthUI ( firebase . auth ( ) )
79
+
80
+ const AuthPrompt = ( { children } ) => {
81
+ const [ transact ] = useTransact ( )
82
+ const [ currentUser ] = useEntity ( { identity : 'currentUser' } )
83
+ const [ client ] = useClient ( )
84
+ React . useEffect ( ( ) => {
85
+ window . emptyDB = client . dbToString ( )
86
+ return firebase . auth ( ) . onAuthStateChanged ( ( user ) => {
87
+ if ( user ) {
88
+ transact ( [ { user : { uid : user . uid , name : user . displayName } } ] )
89
+ client . transactSilently ( [ { currentUser : { identity : 'currentUser' , uid : user . uid } } ] )
90
+ }
91
+ } )
92
+ } , [ ] )
93
+ if ( currentUser . get ( 'uid' ) ) return children
94
+ return < SignIn />
95
+ }
96
+
97
+ const SignIn = ( ) => {
98
+ React . useEffect ( ( ) => {
99
+ firebaseUI . start ( '#firebaseui-auth-container' , {
100
+ signInFlow : 'popup' ,
101
+ signInSuccessUrl : window . location . href ,
102
+ signInOptions : [
103
+ firebase . auth . EmailAuthProvider . PROVIDER_ID ,
104
+ firebase . auth . GoogleAuthProvider . PROVIDER_ID ,
105
+ ] ,
106
+ callbacks : {
107
+ signInSuccessWithAuthResult : ( ) => false
108
+ } ,
109
+ } )
110
+ } , [ ] )
111
+ return < div id = "firebaseui-auth-container" />
112
+ }
113
+
114
+ const Todos = ( ) => (
115
+ < div >
116
+ < DataSaver />
117
+ < SignOut />
118
+ < NewTodo />
119
+ < hr />
120
+ < TodoFilters />
121
+ < TodoList />
122
+ </ div >
123
+ )
124
+
125
+ const DataSaver = ( ) => {
126
+ const [ client ] = useClient ( )
127
+ const [ currentUser ] = useEntity ( { identity : 'currentUser' } )
128
+ const userId = currentUser . get ( 'uid' )
129
+ const transactListener = React . useCallback ( ( changedDatoms ) => {
130
+ const numDatomChanges = changedDatoms . reduce ( ( acc , [ id , attr ] ) => (
131
+ { ...acc , [ id + attr ] : ( acc [ id + attr ] || 0 ) + 1 }
132
+ ) , { } )
133
+ const datomsForFirebase = changedDatoms . filter ( ( [ id , attr , _ , __ , isAdded ] ) => ! ( ! isAdded && numDatomChanges [ id + attr ] > 1 ) )
134
+ datomsForFirebase . forEach ( ( [ id , attr , v , tx , isAdded ] ) => {
135
+ const ref = firebase . database ( ) . ref ( `users/${ userId } /entities/${ id } |${ attr . replace ( '/' , '|' ) } ` )
136
+ isAdded ? ref . set ( [ id , attr , v , tx , isAdded ] ) : ref . remove ( )
137
+ } )
138
+ } , [ userId ] )
139
+ React . useEffect ( ( ) => {
140
+ client . addTransactListener ( transactListener )
141
+ const ref = firebase . database ( ) . ref ( `users/${ userId } /entities` )
142
+ const on = ( action ) => ( ds ) => client . transactSilently ( [ [ action , ...ds . val ( ) ] ] )
143
+ ref . on ( 'child_added' , on ( 'add' ) )
144
+ ref . on ( 'child_removed' , on ( 'retract' ) )
145
+ ref . on ( 'child_changed' , on ( 'add' ) )
146
+ return ( ) => {
147
+ client . removeTransactListener ( )
148
+ ref . off ( 'child_added' , on ( 'add' ) )
149
+ ref . off ( 'child_removed' , on ( 'retract' ) )
150
+ ref . off ( 'child_changed' , on ( 'add' ) )
151
+ }
152
+ } , [ userId ] )
153
+ return null
154
+ }
155
+
156
+ const SignOut = ( ) => {
157
+ const [ client ] = useClient ( )
158
+ return (
159
+ < button
160
+ style = { { float : 'right' } }
161
+ onClick = { ( ) => {
162
+ client . dbFromString ( window . emptyDB )
163
+ firebase . auth ( ) . signOut ( )
164
+ } }
165
+ > Sign Out</ button >
166
+ )
167
+ }
168
+
169
+ const NewTodo = ( ) => {
170
+ const [ transact ] = useTransact ( )
171
+ return (
172
+ < form onSubmit = { e => {
173
+ e . preventDefault ( )
174
+ transact ( [ {
175
+ todo : {
176
+ name : e . target . elements [ 'todo-name' ] . value ,
177
+ createdAt : Date . now ( )
178
+ }
179
+ } ] )
180
+ e . target . reset ( )
181
+ } } >
182
+ < input
183
+ autoFocus
184
+ style = { { fontSize : 20 } }
185
+ type = "text"
186
+ name = "todo-name"
187
+ placeholder = "What needs to be done?"
188
+ autoComplete = "off"
189
+ required
190
+ />
191
+
192
+ < button type = "submit" > Create Todo</ button >
193
+ </ form >
194
+ )
195
+ }
196
+
197
+ const TodoList = ( ) => {
198
+ const [ filters ] = useEntity ( { identity : 'todoFilters' } )
199
+ const [ todos ] = useQuery ( {
200
+ $find : 'todo' ,
201
+ $where : { todo : { name : '$any' } }
202
+ } )
203
+ return (
204
+ < div >
205
+ { todos . filter ( todo => {
206
+ if ( ! filters . get ( 'showCompleted' ) && todo . get ( 'isCompleted' ) ) return false
207
+ if ( filters . get ( 'project' ) && todo . get ( 'project' , 'id' ) !== filters . get ( 'project' ) ) return false
208
+ if ( filters . get ( 'owner' ) && todo . get ( 'owner' , 'id' ) !== filters . get ( 'owner' ) ) return false
209
+ return true
210
+ } ) . sort ( ( a , b ) => a . get ( 'createdAt' ) > b . get ( 'createdAt' ) ? - 1 : 1 )
211
+ . map ( todo => < Todo key = { todo . get ( 'id' ) } id = { todo . get ( 'id' ) } /> ) }
212
+ </ div >
213
+ )
214
+ }
215
+
216
+ // PERFORMANCE: By accepting an `id` prop instead of a whole `todo` entity
217
+ // this component stays disconnected from the useQuery in the parent TodoList.
218
+ // useEntity creates a separate scope for every Todo so changes to TodoList
219
+ // or sibling Todos don't trigger unnecessary re-renders.
220
+ const Todo = React . memo ( ( { id } ) => {
221
+ const [ todo ] = useEntity ( id )
222
+ return (
223
+ < div >
224
+ < div style = { { display : 'flex' , flexDirection : 'row' , alignItems : 'flex-end' , paddingTop : 20 } } >
225
+ < TodoCheck todo = { todo } />
226
+ < TodoName todo = { todo } />
227
+ </ div >
228
+ < div >
229
+ < TodoProject todo = { todo } />
230
+ ·
231
+ < TodoOwner todo = { todo } />
232
+ ·
233
+ < TodoDelete todo = { todo } />
234
+ </ div >
235
+ < small style = { { color : 'grey' } } >
236
+ { new Date ( todo . get ( 'createdAt' ) ) . toLocaleString ( ) }
237
+ </ small >
238
+ </ div >
239
+ )
240
+ } )
241
+
242
+ const TodoCheck = ( { todo } ) => {
243
+ const [ transact ] = useTransact ( )
244
+ return (
245
+ < input
246
+ type = "checkbox"
247
+ style = { { width : 20 , height : 20 , cursor : 'pointer' } }
248
+ checked = { ! ! todo . get ( 'isCompleted' ) }
249
+ onChange = { e => transact ( [ { todo : { id : todo . get ( 'id' ) , isCompleted : e . target . checked } } ] ) }
250
+ />
251
+ )
252
+ }
253
+
254
+ const TodoName = ( { todo } ) => {
255
+ const [ transact ] = useTransact ( )
256
+ return (
257
+ < input
258
+ style = { {
259
+ border : 'none' , fontSize : 20 , marginTop : - 2 , cursor : 'pointer' ,
260
+ ...todo . get ( 'isCompleted' ) && { textDecoration : 'line-through ' }
261
+ } }
262
+ value = { todo . get ( 'name' ) || '' }
263
+ onChange = { e => transact ( [ { todo : { id : todo . get ( 'id' ) , name : e . target . value } } ] ) }
264
+ />
265
+ )
266
+ }
267
+
268
+ const TodoProject = ( { todo } ) => {
269
+ const [ transact ] = useTransact ( )
270
+ return (
271
+ < EntitySelect
272
+ label = "Project"
273
+ entityType = "project"
274
+ value = { todo . get ( 'project' , 'id' ) }
275
+ onChange = { project => transact ( [ { todo : { id : todo . get ( 'id' ) , project } } ] ) }
276
+ />
277
+ )
278
+ }
279
+
280
+ const TodoOwner = ( { todo } ) => {
281
+ const [ transact ] = useTransact ( )
282
+ return (
283
+ < EntitySelect
284
+ label = "Owner"
285
+ entityType = "user"
286
+ value = { todo . get ( 'owner' , 'id' ) }
287
+ onChange = { owner => transact ( [ { todo : { id : todo . get ( 'id' ) , owner } } ] ) }
288
+ />
289
+ )
290
+ }
291
+
292
+ const TodoDelete = ( { todo } ) => {
293
+ const [ transact ] = useTransact ( )
294
+ return (
295
+ < button onClick = { ( ) => transact ( [ [ 'retractEntity' , todo . get ( 'id' ) ] ] ) } >
296
+ Delete
297
+ </ button >
298
+ )
299
+ }
300
+
301
+ const TodoFilters = ( ) => {
302
+ const [ filters ] = useEntity ( { identity : 'todoFilters' } )
303
+ const [ client ] = useClient ( )
304
+ return (
305
+ < div >
306
+ Filter by:
307
+ < label > Show Completed?
308
+ < input
309
+ type = "checkbox"
310
+ checked = { filters . get ( 'showCompleted' ) }
311
+ onChange = { e => client . transactSilently ( [ { todoFilter : { id : filters . get ( 'id' ) , showCompleted : e . target . checked } } ] ) }
312
+ />
313
+ </ label >
314
+ ·
315
+ < EntitySelect
316
+ label = "Project"
317
+ entityType = "project"
318
+ value = { filters . get ( 'project' ) }
319
+ onChange = { project => client . transactSilently ( [ { todoFilter : { id : filters . get ( 'id' ) , project } } ] ) }
320
+ />
321
+ ·
322
+ < EntitySelect
323
+ label = "Owner"
324
+ entityType = "user"
325
+ value = { filters . get ( 'owner' ) }
326
+ onChange = { owner => client . transactSilently ( [ { todoFilter : { id : filters . get ( 'id' ) , owner } } ] ) }
327
+ />
328
+ </ div >
329
+ )
330
+ }
331
+
332
+ const EntitySelect = React . memo ( ( { label, entityType, value, onChange } ) => {
333
+ const [ entities ] = useQuery ( {
334
+ $find : entityType ,
335
+ $where : { [ entityType ] : { name : '$any' } }
336
+ } )
337
+ return (
338
+ < label > { label } :
339
+ < select
340
+ name = { entityType }
341
+ value = { value || '' }
342
+ onChange = { e => onChange && onChange ( Number ( e . target . value ) || null ) }
343
+ >
344
+ < option key = "-" value = "" > </ option >
345
+ { entities . map ( entity => (
346
+ < option key = { entity . get ( 'id' ) } value = { entity . get ( 'id' ) } >
347
+ { entity . get ( 'name' ) }
348
+ </ option >
349
+ ) ) }
350
+ </ select >
351
+ </ label >
352
+ )
353
+ } )
0 commit comments