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