Skip to content

Commit e02a111

Browse files
committed
SQLWorker initialization and Firebase e-mail link auth
1 parent b05d119 commit e02a111

21 files changed

+1407
-378
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@
2121
npm-debug.log*
2222
yarn-debug.log*
2323
yarn-error.log*
24+
25+
public/xkcd_color_survey.sqlite
26+
.env

config-overrides.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = function override(config, env) {
2+
config.module.rules.push({
3+
test: /\.worker\.js$/,
4+
use: { loader: 'worker-loader' },
5+
});
6+
return config;
7+
};

package.json

+22-9
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
{
2-
"name": "xkcd-color-survey",
2+
"name": "xkcd-color-answers",
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
6-
"@testing-library/jest-dom": "^4.2.4",
7-
"@testing-library/react": "^9.3.2",
8-
"@testing-library/user-event": "^7.1.2",
6+
"@material-ui/core": "^4.9.5",
7+
"@material-ui/icons": "^4.9.1",
8+
"@material-ui/lab": "^4.0.0-alpha.45",
9+
"firebase": "^7.10.0",
10+
"lodash": "^4.17.15",
911
"react": "^16.12.0",
1012
"react-dom": "^16.12.0",
11-
"react-scripts": "3.4.0"
13+
"react-scripts": "3.4.0",
14+
"sql.js": "^1.1.0",
15+
"use-query-params": "^0.6.0"
16+
},
17+
"devDependencies": {
18+
"react-app-rewired": "^2.1.5",
19+
"worker-loader": "^2.0.0"
1220
},
1321
"scripts": {
14-
"start": "react-scripts start",
15-
"build": "react-scripts build",
16-
"test": "react-scripts test",
17-
"eject": "react-scripts eject"
22+
"start": "react-app-rewired start",
23+
"build": "react-app-rewired build",
24+
"test": "react-app-rewired test",
25+
"eject": "react-app-rewired eject"
1826
},
1927
"eslintConfig": {
2028
"extends": "react-app"
@@ -30,5 +38,10 @@
3038
"last 1 firefox version",
3139
"last 1 safari version"
3240
]
41+
},
42+
"prettier": {
43+
"semi": true,
44+
"singleQuote": true,
45+
"trailingComma": "es5"
3346
}
3447
}

public/favicon.ico

12 KB
Binary file not shown.

public/index.html

+5-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
1616
-->
1717
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
18+
<link
19+
rel="stylesheet"
20+
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
21+
/>
1822
<!--
1923
Notice the use of %PUBLIC_URL% in the tags above.
2024
It will be replaced with the URL of the `public` folder during the build.
@@ -24,7 +28,7 @@
2428
work correctly both with client-side routing and a non-root public URL.
2529
Learn how to configure a non-root public URL by running `npm run build`.
2630
-->
27-
<title>React App</title>
31+
<title>XKCD color answers</title>
2832
</head>
2933
<body>
3034
<noscript>You need to enable JavaScript to run this app.</noscript>

public/logo192.png

9.39 KB
Loading

public/logo512.png

37.4 KB
Loading

public/sql-wasm.wasm

1.4 MB
Binary file not shown.

src/App.css

-38
This file was deleted.

src/App.js

+66-19
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,72 @@
1-
import React from 'react';
2-
import logo from './logo.svg';
3-
import './App.css';
1+
import React, { useEffect, useRef, useState } from 'react';
2+
import { useQueryParam, StringParam } from 'use-query-params';
3+
4+
import Box from '@material-ui/core/Box';
5+
6+
import ErrorAlert from './components/ErrorAlert';
7+
import Login from './components/Login';
8+
import SQLWorker from './sql.worker';
49

510
function App() {
11+
const workerRef = useRef(null);
12+
const [apiKey] = useQueryParam('apiKey', StringParam);
13+
const [data, setData] = useState(null);
14+
const [dbReady, setDbReady] = useState(false);
15+
const [workerError, setWorkerError] = useState(null);
16+
const [user, setUser] = useState(null);
17+
18+
useEffect(() => {
19+
const worker = new SQLWorker();
20+
workerRef.current = worker;
21+
22+
worker.onmessage = ({ data: { payload, type } }) => {
23+
console.log(`App.js received message of type: ${type}`);
24+
switch (type) {
25+
case 'init': {
26+
setDbReady(true);
27+
break;
28+
}
29+
case 'data': {
30+
setData(payload?.data);
31+
break;
32+
}
33+
default: {
34+
setWorkerError(
35+
new Error(`Unrecognized message type from SQLWorker: ${type}`)
36+
);
37+
}
38+
}
39+
};
40+
41+
worker.onerror = error => {
42+
setWorkerError(error);
43+
};
44+
}, []);
45+
46+
useEffect(() => {
47+
if (dbReady) {
48+
const worker = workerRef.current;
49+
50+
worker.postMessage({
51+
type: 'query',
52+
payload: {
53+
query:
54+
'SELECT colorname, COUNT(colorname) as count FROM answers GROUP BY colorname HAVING count >= 25 ORDER BY count DESC;',
55+
},
56+
});
57+
}
58+
}, [dbReady]);
59+
60+
if (workerError) {
61+
return (
62+
<ErrorAlert clearError={() => setWorkerError(null)} error={workerError} />
63+
);
64+
}
65+
666
return (
7-
<div className="App">
8-
<header className="App-header">
9-
<img src={logo} className="App-logo" alt="logo" />
10-
<p>
11-
Edit <code>src/App.js</code> and save to reload.
12-
</p>
13-
<a
14-
className="App-link"
15-
href="https://reactjs.org"
16-
target="_blank"
17-
rel="noopener noreferrer"
18-
>
19-
Learn React
20-
</a>
21-
</header>
22-
</div>
67+
<Box height="100vh" width="100vw">
68+
{!user && <Login apiKey={apiKey} setUser={setUser} user={user} />}
69+
</Box>
2370
);
2471
}
2572

src/App.test.js

-9
This file was deleted.

src/components/ErrorAlert.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react';
2+
3+
import Alert from '@material-ui/lab/Alert';
4+
import Snackbar from '@material-ui/core/Snackbar';
5+
6+
export default function ErrorAlert({ ActionComponent, clearError, error }) {
7+
return (
8+
<Snackbar
9+
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
10+
autoHideDuration={5000}
11+
onClose={clearError}
12+
open={!!error}
13+
>
14+
<Alert action={ActionComponent} severity="error">
15+
{error?.message || 'Unknown error'}
16+
</Alert>
17+
</Snackbar>
18+
);
19+
}

src/components/Login.js

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React, { useState } from 'react';
2+
3+
import Alert from '@material-ui/lab/Alert';
4+
import Button from '@material-ui/core/Button';
5+
import Dialog from '@material-ui/core/Dialog';
6+
import DialogActions from '@material-ui/core/DialogActions';
7+
import DialogContent from '@material-ui/core/DialogContent';
8+
import DialogContentText from '@material-ui/core/DialogContentText';
9+
import DialogTitle from '@material-ui/core/DialogTitle';
10+
import HomeIcon from '@material-ui/icons/Home';
11+
import Snackbar from '@material-ui/core/Snackbar';
12+
import TextField from '@material-ui/core/TextField';
13+
14+
import ErrorAlert from './ErrorAlert';
15+
import useFirebaseAuth from '../hooks/useFirebaseAuth';
16+
17+
export default function Login({ apiKey, setUser, user }) {
18+
const [anonymous, setAnonymous] = useState(false);
19+
const [email, setEmail] = useState('');
20+
const {
21+
clearError,
22+
loginError,
23+
onSubmit,
24+
resetSubmission,
25+
submitted,
26+
} = useFirebaseAuth(email, setUser);
27+
28+
if (loginError) {
29+
return (
30+
<ErrorAlert
31+
ActionComponent={
32+
<Button href="/" size="small" startIcon={<HomeIcon />}>
33+
Return home
34+
</Button>
35+
}
36+
clearError={clearError}
37+
error={loginError}
38+
/>
39+
);
40+
}
41+
42+
if (submitted) {
43+
return (
44+
<Snackbar
45+
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
46+
autoHideDuration={5000}
47+
onClose={resetSubmission}
48+
open={submitted}
49+
>
50+
<Alert severity="info">{`Sent login link to ${email}!`}</Alert>
51+
</Snackbar>
52+
);
53+
}
54+
55+
if (anonymous || apiKey || user) {
56+
return null;
57+
}
58+
59+
return (
60+
<Dialog open={true} aria-labelledby="form-dialog-title">
61+
<DialogTitle id="form-dialog-title">
62+
<span role="img" aria-label="ogre emoji">
63+
👹
64+
</span>
65+
&nbsp;Login
66+
</DialogTitle>
67+
<DialogContent>
68+
<DialogContentText>
69+
You give e-mail. Then&nbsp;&nbsp;
70+
<span role="img" aria-label="ogre emoji">
71+
👹
72+
</span>
73+
&nbsp;sends sign-in link.
74+
</DialogContentText>
75+
<TextField
76+
autoFocus
77+
fullWidth
78+
id="name"
79+
label="Email Address"
80+
margin="dense"
81+
onChange={e => setEmail(e.target.value)}
82+
type="email"
83+
value={email}
84+
/>
85+
</DialogContent>
86+
<DialogActions>
87+
<Button color="secondary" onClick={() => setAnonymous(true)}>
88+
Cancel
89+
</Button>
90+
<Button onClick={onSubmit} color="primary">
91+
Login
92+
</Button>
93+
</DialogActions>
94+
</Dialog>
95+
);
96+
}

0 commit comments

Comments
 (0)