├── .eslintignore
├── .gitignore
├── .prettierignore
├── .prettierrc
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── __tests__
│ └── index.js
├── bootstrap
│ ├── firebase.js
│ └── index.js
├── context
│ ├── index.js
│ └── user.js
├── index.js
├── screens
│ ├── about.js
│ ├── chat.js
│ ├── home.js
│ └── login.js
└── setupTests.js
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | coverage
4 | public
5 | .docz
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | coverage
4 | public
5 | .docz
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSpacing": false,
4 | "jsxBracketSameLine": false,
5 | "printWidth": 80,
6 | "proseWrap": "always",
7 | "semi": false,
8 | "singleQuote": true,
9 | "tabWidth": 2,
10 | "trailingComma": "all",
11 | "useTabs": false
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@emotion/core": "^10.0.10",
7 | "@emotion/styled": "^10.0.10",
8 | "@reach/router": "^1.2.1",
9 | "emotion-theming": "^10.0.10",
10 | "firebase": "^5.10.0",
11 | "jest-dom": "^3.1.3",
12 | "lodash": "^4.17.11",
13 | "react": "^16.8.6",
14 | "react-async": "^6.0.2",
15 | "react-dom": "^16.8.6",
16 | "react-firebase-hooks": "^1.2.0",
17 | "react-scripts": "3.0.0-next.68",
18 | "react-testing-library": "^6.1.2"
19 | },
20 | "devDependencies": {
21 | "cross-env": "^5.2.0",
22 | "husky": "^1.1.4",
23 | "jest-emotion": "^10.0.10",
24 | "lint-staged": "^8.1.5",
25 | "npm-run-all": "^4.1.5",
26 | "prettier": "^1.17.0"
27 | },
28 | "scripts": {
29 | "start": "react-scripts start",
30 | "build": "react-scripts build",
31 | "test": "react-scripts test",
32 | "coverage": "npm run test -- --coverage --no-watch",
33 | "format": "prettier --write \"**/*.+(js|json|css|md|mdx|html)\"",
34 | "validate": "npm-run-all --parallel coverage build"
35 | },
36 | "eslintConfig": {
37 | "extends": "react-app"
38 | },
39 | "husky": {
40 | "hooks": {
41 | "pre-commit": "lint-staged && npm run build"
42 | }
43 | },
44 | "jest": {
45 | "snapshotSerializers": [
46 | "jest-emotion"
47 | ]
48 | },
49 | "browserslist": {
50 | "production": [
51 | ">0.2%",
52 | "not dead",
53 | "not op_mini all"
54 | ],
55 | "development": [
56 | "last 1 chrome version",
57 | "last 1 firefox version",
58 | "last 1 safari version"
59 | ]
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kentcdodds/geo-chat/609e9b09627e1c44da119562c9180c0a1b6b9ded/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/__tests__/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {render} from 'react-testing-library'
3 |
4 | test('works', () => {
5 | const {container} = render(hi
)
6 | expect(container.firstChild).toHaveTextContent('hi')
7 | })
8 |
--------------------------------------------------------------------------------
/src/bootstrap/firebase.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase/app'
2 | import 'firebase/auth'
3 | import 'firebase/firestore'
4 |
5 | firebase.initializeApp({
6 | apiKey: 'AIzaSyCSA6P3Yj0wGu4Gsr6Hx6w8dWaNjxwyuK4',
7 | authDomain: 'the-geo-chat-app.firebaseapp.com',
8 | databaseURL: 'https://the-geo-chat-app.firebaseio.com',
9 | projectId: 'the-geo-chat-app',
10 | storageBucket: 'the-geo-chat-app.appspot.com',
11 | messagingSenderId: '879802748968',
12 | })
13 |
--------------------------------------------------------------------------------
/src/bootstrap/index.js:
--------------------------------------------------------------------------------
1 | import './firebase'
2 |
--------------------------------------------------------------------------------
/src/context/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {UserProvider} from './user'
3 |
4 | function AppProviders({children}) {
5 | return {children}
6 | }
7 |
8 | export default AppProviders
9 |
--------------------------------------------------------------------------------
/src/context/user.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import isMatch from 'lodash/isMatch'
3 | import firebase from 'firebase/app'
4 | import {useAuthState} from 'react-firebase-hooks/auth'
5 | import {useDocument} from 'react-firebase-hooks/firestore'
6 | import {navigate} from '@reach/router'
7 |
8 | function loginWithTwitter() {
9 | const provider = new firebase.auth.TwitterAuthProvider()
10 | firebase.auth().signInWithPopup(provider)
11 | }
12 |
13 | function loginWithGitHub() {
14 | const provider = new firebase.auth.GithubAuthProvider()
15 | firebase.auth().signInWithPopup(provider)
16 | }
17 |
18 | function logout() {
19 | firebase.auth().signOut()
20 | }
21 |
22 | const UserContext = React.createContext()
23 |
24 | function Authenticate(props) {
25 | const {initialising: initializing, user} = useAuthState(firebase.auth())
26 | const context = React.useMemo(() => {
27 | return {
28 | loading: initializing,
29 | user,
30 | loginWithTwitter,
31 | loginWithGitHub,
32 | logout,
33 | }
34 | }, [initializing, user])
35 |
36 | if (initializing || !user) {
37 | return
38 | }
39 | return
40 | }
41 |
42 | function UserProvider(props) {
43 | // spelling is weird, so we'll fix it :)
44 | const {authenticatedUser} = props
45 | const {error, loading, value: userDoc} = useDocument(
46 | firebase
47 | .firestore()
48 | .collection('users')
49 | .doc(authenticatedUser.uid),
50 | )
51 |
52 | // update users collection with old/missing data
53 | React.useEffect(() => {
54 | if (!userDoc) {
55 | return
56 | }
57 | const userData = userDoc.data()
58 | const authenticatedUserData = {
59 | id: authenticatedUser.uid,
60 | displayName: authenticatedUser.displayName,
61 | email: authenticatedUser.email,
62 | photoURL: authenticatedUser.photoURL,
63 | providers: authenticatedUser.providerData.map(p => ({
64 | providerId: p.providerId,
65 | uid: p.uid,
66 | })),
67 | }
68 | if (!isMatch(userData, authenticatedUserData)) {
69 | userDoc.ref.set(authenticatedUserData)
70 | }
71 | }, [authenticatedUser, userDoc])
72 |
73 | const context = React.useMemo(() => {
74 | return {
75 | loading,
76 | user: userDoc ? userDoc.data() : null,
77 | error,
78 | loginWithTwitter,
79 | loginWithGitHub,
80 | logout,
81 | }
82 | }, [loading, userDoc, error])
83 | return
84 | }
85 |
86 | function useUser() {
87 | const context = React.useContext(UserContext)
88 | if (!context) {
89 | throw new Error(
90 | `useUser must be used within a component that's rendered within the UserProvider`,
91 | )
92 | }
93 | return context
94 | }
95 |
96 | function useAuthenticatedRedirect(destination = '/chat') {
97 | const {user} = useUser()
98 | if (user) {
99 | navigate(destination)
100 | }
101 | }
102 |
103 | function useUnauthenticatedRedirect(destination = '/login') {
104 | const {user} = useUser()
105 | if (user) {
106 | navigate(destination)
107 | }
108 | }
109 |
110 | export {
111 | Authenticate as UserProvider,
112 | useUser,
113 | useUnauthenticatedRedirect,
114 | useAuthenticatedRedirect,
115 | }
116 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import './bootstrap'
3 |
4 | import {jsx} from '@emotion/core'
5 | import ReactDOM from 'react-dom'
6 | import {Router} from '@reach/router'
7 | import AppProviders from './context'
8 | import {useUser} from './context/user'
9 | import Home from './screens/home'
10 | import Chat from './screens/chat'
11 | import Login from './screens/login'
12 | import About from './screens/about'
13 |
14 | function Routes() {
15 | const {loading} = useUser()
16 | if (loading) {
17 | return 'loading ...'
18 | }
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | function App() {
30 | return (
31 |
32 |
33 |
34 | )
35 | }
36 |
37 | ReactDOM.render(, document.getElementById('root'))
38 |
--------------------------------------------------------------------------------
/src/screens/about.js:
--------------------------------------------------------------------------------
1 | function About() {
2 | return 'About'
3 | }
4 |
5 | export default About
6 |
--------------------------------------------------------------------------------
/src/screens/chat.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import {jsx} from '@emotion/core'
3 | import React from 'react'
4 | import firebase from 'firebase/app'
5 | import {useCollection} from 'react-firebase-hooks/firestore'
6 | import {Redirect} from '@reach/router'
7 | import {useUser} from '../context/user'
8 |
9 | function Chat() {
10 | const {user, logout} = useUser()
11 | return (
12 |
13 |
14 |

19 |
{user.displayName}
20 |
21 |
22 |
{JSON.stringify(user, null, 2)}
23 |
24 |
25 | )
26 | }
27 |
28 | const geoPositionReducer = (state, action) => ({...state, ...action})
29 |
30 | function useGeoLocation(positionOptions) {
31 | const [state, setState] = React.useReducer(geoPositionReducer, {
32 | position: null,
33 | loading: true,
34 | error: null,
35 | })
36 | React.useEffect(() => {
37 | setState({loading: true})
38 |
39 | function onPosition(position) {
40 | setState({position, loading: false, error: null})
41 | }
42 | function onError(error) {
43 | setState({position: null, loading: false, error})
44 | }
45 |
46 | navigator.geolocation.getCurrentPosition(
47 | onPosition,
48 | onError,
49 | positionOptions,
50 | )
51 |
52 | const listener = navigator.geolocation.watchPosition(
53 | onPosition,
54 | onError,
55 | positionOptions,
56 | )
57 |
58 | return () => navigator.geolocation.clearWatch(listener)
59 | }, [positionOptions])
60 |
61 | return state
62 | }
63 |
64 | const getRoomId = ({latitude, longitude}) =>
65 | `${(latitude * 10).toFixed()}_${(longitude * 10).toFixed()}`
66 |
67 | function ChatMessages() {
68 | const {position, loading, error} = useGeoLocation()
69 | if (loading) {
70 | return '... calculating your room based on your location ...'
71 | }
72 | if (error) {
73 | console.error(error)
74 | return '... there was an error determining your location ...'
75 | }
76 | const roomId = getRoomId(position.coords)
77 | return
78 | }
79 |
80 | function ChatMessagesWithRoomId({roomId}) {
81 | const {user} = useUser()
82 | const messagesRef = firebase
83 | .firestore()
84 | .collection('rooms')
85 | .doc(roomId)
86 | .collection('messages')
87 | const {error, loading, value: messagesSnapshot} = useCollection(
88 | messagesRef.orderBy('timestamp', 'desc').limit(20),
89 | )
90 | if (loading) {
91 | return '... loading room chats ...'
92 | }
93 | if (error) {
94 | console.error(error)
95 | return 'There was an error loading the room chats'
96 | }
97 |
98 | function handleSubmit(event) {
99 | event.preventDefault()
100 | const {message} = event.target.elements
101 |
102 | messagesRef.add({
103 | authorId: user.id,
104 | message: message.value,
105 | timestamp: firebase.firestore.FieldValue.serverTimestamp(),
106 | })
107 | }
108 |
109 | return (
110 |
111 |
112 | {messagesSnapshot.docs.reverse().map(d => (
113 |
{d.data().message}
114 | ))}
115 |
116 |
121 |
122 | )
123 | }
124 |
125 | function ChatRedirect(props) {
126 | const {user} = useUser()
127 | if (!user) {
128 | return
129 | }
130 | return
131 | }
132 |
133 | export default ChatRedirect
134 |
--------------------------------------------------------------------------------
/src/screens/home.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import {jsx} from '@emotion/core'
3 |
4 | import {Link, Redirect} from '@reach/router'
5 | import {useUser} from '../context/user'
6 |
7 | function Home() {
8 | const {user} = useUser()
9 | if (user) {
10 | return
11 | }
12 | return (
13 |
14 | Home
15 | Login
16 |
17 | )
18 | }
19 |
20 | export default Home
21 |
--------------------------------------------------------------------------------
/src/screens/login.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {useUser} from '../context/user'
3 | import {Redirect} from '@reach/router'
4 |
5 | function Login() {
6 | const {user, loginWithGitHub, loginWithTwitter} = useUser()
7 | if (user) {
8 | return
9 | }
10 | return (
11 |
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default Login
19 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import 'jest-dom/extend-expect'
2 | import 'react-testing-library/cleanup-after-each'
3 |
--------------------------------------------------------------------------------