...
 
Commits (2)
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "Edupack",
"name": "Edupack",
"icons": [
{
"src": "favicon.ico",
......
import React, { useState, createContext, useEffect, memo } from 'react';
import { UserInfo, fetchUserInfo } from './api/info';
import Login from './pages/Login';
import Home from './pages/Home';
import Preloader from './pages/Preloader';
import { ReactiveDB, openDatabase } from './data/database';
enum Page {
LOADING = 0,
LOGIN = 1,
HOME = 2
}
export interface AppState {
page: Page,
userInfo?: UserInfo,
database?: ReactiveDB
};
export const AppStateContext = createContext<AppState & { setState: (state: AppState) => void }>({} as any);
export default () => {
let [ state, setState ] = useState<AppState>({
page: Page.LOADING,
});
async function updateState(userInfo?: UserInfo) {
if (userInfo) {
localStorage.setItem('user', JSON.stringify(userInfo));
setState({
...state,
page: Page.HOME,
userInfo,
database: new ReactiveDB(userInfo.id)
});
} else {
setState({
...state,
page: Page.LOGIN,
});
}
}
const { page } = state;
useEffect(() => {
fetchUserInfo()
.then(res => updateState(res))
.catch(() => {
let user = localStorage.getItem('user');
if (user === null) {
updateState();
} else {
let obj = JSON.parse(user),
keys = JSON.stringify(Object.keys(obj));
updateState(
keys === JSON.stringify([ 'id', 'name', 'givenName', 'email' ])
? obj : undefined
);
}
})
// eslint-disable-next-line
}, []);
return (
<AppStateContext.Provider
value={{ ...state, setState }}>
{
page === 2 ? <Home {...state} /> :
page === 1 ? <Login /> :
<Preloader />
}
</AppStateContext.Provider>
);
};
\ No newline at end of file
......@@ -7,7 +7,7 @@ export enum AssignmentType {
export interface Assignment {
id: string,
nonce: string,
updatedAt: string | null,
updatedAt: string,
subject: string,
type: AssignmentType,
title: string,
......
import Axios from 'axios';
export interface UserInfo {
id: string,
givenName: string,
name: string,
email: string,
......
import React, { Fragment, useState, memo } from 'react';
import { ListItem, ListItemIcon, ListItemText, Typography, Card, CardActions, Button } from '@material-ui/core';
import DoneIcon from '@material-ui/icons/Done';
import EditIcon from '@material-ui/icons/Edit';
import DeleteIcon from '@material-ui/icons/Delete';
import { SubjectIcon } from '../util/Icon';
import moment from 'moment';
import { Assignment } from '../api/assignments';
export function validDate(d: any): boolean {
return moment(d).isValid();
}
export default memo((props: Assignment & { active: boolean, select: (id: string) => boolean, edit: (id: string) => void, delete: (task: Assignment) => void }) => {
const due = props.dueDate || undefined;
return (
<Fragment>
<ListItem button
onClick={() => props.select(props.id)}>
<ListItemIcon>
<SubjectIcon subject={props.subject} />
</ListItemIcon>
<ListItemText
primary={
<Fragment>
<Typography
variant="overline"
style={{ lineHeight: 0, fontSize: '0.6em' }}>
{props.subject}
{
props.class &&
` — ${props.class}`
}
{
validDate(due) &&
` · due ${moment(due).fromNow()}`
}
</Typography>
<br />
{props.title}
</Fragment>
}
secondary={
<Fragment>
{props.description}
</Fragment>
}
/>
</ListItem>
{
props.active &&
<Card style={{ boxShadow: 'none' }}>
<CardActions>
<Button
startIcon={<DoneIcon />}
size="small">
done
</Button>
<Button
startIcon={<EditIcon />}
size="small"
onClick={() => props.edit(props.id)}>
edit
</Button>
<Button
startIcon={<DeleteIcon />}
size="small"
onClick={() => props.delete(props)}>
delete
</Button>
</CardActions>
</Card>
}
</Fragment>
);
});
\ No newline at end of file
import Dexie from 'dexie';
import { omit, assign } from 'lodash';
import { EventEmitter } from 'events';
import { Assignment, fetchAssignments, updateAssignment, deleteAssignment } from '../api/assignments';
export type AssignmentDB = Dexie & {
entries: Dexie.Table<Assignment, string>,
};
export function openDatabase(userId: string) {
let db = new Dexie('assignments_' + userId);
db.version(1)
.stores({
entries: '&id,&nonce,updatedAt,subject,type,title,description,class,dueDate,complete'
});
return db as AssignmentDB;
}
export class ReactiveDB extends EventEmitter {
db: AssignmentDB;
syncing: boolean;
constructor(userId: string) {
super();
this.db = openDatabase(userId);
}
async sync(): Promise<boolean> {
if (this.syncing)
return true;
this.emit('beginSync');
this.syncing = true;
try {
let remote = await fetchAssignments();
let local = await this.db.entries.toArray();
// process local_ and upload
const remoteNonces = remote.map(x => x.nonce);
for (let task of local) {
if (task.id.startsWith('local_')) {
// check nonce does not exist in remote, otherwise use that object
if (remoteNonces.indexOf(task.nonce) > -1) {
assign(task, remote[remoteNonces.indexOf(task.nonce)]);
} else {
let { id } = await updateAssignment(omit(task, [ 'id' ]) as any);
task.id = id;
}
await this.db.entries
.where('nonce')
.equals(task.nonce)
.delete();
await this.db.entries
.add(task);
}
}
remote = await fetchAssignments();
// delete non-existant local entries
const remoteIds = remote.map(x => x.id);
for (let task of local) {
if (remoteIds.indexOf(task.id) === -1) {
await this.db.entries
.where('nonce')
.equals(task.nonce)
.delete();
}
}
local.filter(x => remoteIds.indexOf(x.id) > 0);
// download new entries
const localIds = local.map(x => x.id);
for (let task of remote) {
if (localIds.indexOf(task.id) === -1) {
await this.db.entries
.add(task);
}
}
// compare local and remote entries and move as needed
for (let task of local) {
let remoteTask = remote[remoteIds.indexOf(task.id)];
if (new Date(task.updatedAt) > new Date(remoteTask.updatedAt)) {
await updateAssignment(task as any);
} else {
assign(task, remoteTask);
await this.db.entries
.update(task.id, omit(task, [ 'id' ]));
}
}
} catch (e) {
console.error(`Failed to sync! ${e}`);
}
this.syncing = false;
this.emit('endSync');
return false;
}
async update(task: Assignment) {
if (!task.id.startsWith('local_')) {
await updateAssignment(task as any);
await this.db.entries
.update(task.id, omit(task, [ 'id' ]));
} else {
await this.db.entries
.add(task);
}
this.emit('update');
this.sync();
}
async delete(task: Assignment) {
if (!task.id.startsWith('local_')) {
await deleteAssignment(task.id);
}
await this.db.entries
.where('nonce')
.equals(task.nonce)
.delete();
this.emit('update');
this.sync();
}
}
\ No newline at end of file
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<h1>edupack</h1>, document.getElementById('root'));
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
serviceWorker.register();
......@@ -7,45 +7,9 @@ import DateFnsUtils from '@date-io/date-fns';
import { fetchUserInfo, UserInfo } from '../api/info';
import App from './App';
export const UserContext = createContext<UserInfo>({} as any),
useStyles = makeStyles(theme => (
{
'@global': {
body: {
backgroundColor: theme.palette.common.white,
},
},
appBar: {
position: 'relative',
backgroundColor: theme.palette.grey[800],
},
title: {
marginLeft: theme.spacing(2),
flex: 1,
},
paper: {
marginTop: theme.spacing(8),
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main,
},
form: {
width: '100%',
marginTop: theme.spacing(1),
},
submit: {
margin: theme.spacing(3, 0, 2),
},
}
));
export const UserContext = createContext<UserInfo>({} as any);
const Loader = () => {
const classes = useStyles();
let [ userInfo, setUserInfo ] = useState<UserInfo>({} as any);
let [ status, setStatus ] = useState(0);
if (status === 0) {
......@@ -61,13 +25,7 @@ const Loader = () => {
if (status < 2) {
return (
<Container component="main" maxWidth="xs">
<div className={classes.paper}>
<Typography component="h1" variant="h5">
Loading Edupack...
</Typography>
</div>
</Container>
);
}
......
import React from 'react';
import React, { useState, useContext, Fragment, useEffect, memo, Component } from 'react';
import { List, Typography, Container, Tooltip, makeStyles, Button, withStyles, Dialog, DialogTitle, DialogActions } from '@material-ui/core';
export default () => {
return (
<h1>test</h1>
);
};
\ No newline at end of file
import ModifyDialog from './dialogs/ModifyAssignment';
import { AppStateContext, AppState } from '../App';
import Task, { validDate } from '../components/Task';
import { Assignment } from '../api/assignments';
import moment from 'moment';
import AddIcon from '@material-ui/icons/Add';
interface HomeState {
syncing: boolean,
editing: boolean,
tasks: Assignment[],
selected?: string,
lastSync: Date,
confirmDelete: boolean,
}
const useStyles = withStyles(theme => (
{
paper: {
marginTop: theme.spacing(8),
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
}
));
function sort(source: any[]) {
const sorted = source.filter(x => validDate(x.dueDate));
sorted.sort((a: any, b: any) => +new Date(a.dueDate) - +new Date(b.dueDate));
return [
...sorted,
...source.filter(x => !validDate(x.dueDate))
];
}
export default useStyles(class extends Component<AppState & { classes: any }, HomeState> {
constructor(props: AppState & { classes: any }) {
super(props);
const lastRealSync = localStorage.getItem('lastSync');
this.state = {
syncing: true,
editing: false,
tasks: [],
lastSync: lastRealSync ? new Date(parseInt(lastRealSync)) : new Date(),
confirmDelete: false,
}
this.selectTask = this.selectTask.bind(this);
this.editTask = this.editTask.bind(this);
this.deleteTask = this.deleteTask.bind(this);
this.sync = this.sync.bind(this);
this.loadTasks = this.loadTasks.bind(this);
this.beginSync = this.beginSync.bind(this);
this.endSync = this.endSync.bind(this);
}
async getTasks() {
const database = this.props.database;
if (!database) return [];
return sort(await database.db.entries.toArray());
}
async loadTasks() {
this.setState({
tasks: await this.getTasks()
});
}
async sync() {
const database = this.props.database;
if (typeof database === 'undefined')
return;
this.setState({
tasks: await this.getTasks()
});
database.sync();
}
beginSync() {
this.setState({
syncing: true
});
}
async endSync() {
let now = new Date();
this.setState({
syncing: false,
tasks: await this.getTasks(),
lastSync: now
})
localStorage.setItem('lastSync', (+now).toString());
}
componentDidMount() {
this.sync();
let db = this.props.database;
db && db.addListener('update', this.loadTasks);
db && db.addListener('beginSync', this.beginSync);
db && db.addListener('endSync', this.endSync);
}
componentWillUnmount() {
let db = this.props.database;
db && db.removeListener('update', this.loadTasks);
db && db.removeListener('beginSync', this.beginSync);
db && db.removeListener('endSync', this.endSync);
}
selectTask(id: string) {
let newState = this.state.selected === id ? undefined : id;
this.setState({
selected: newState
});
return newState === id;
}
editTask(id: string) {
this.setState({
editing: true
})
}
deleteTask(task: Assignment) {
this.setState({
confirmDelete: true
});
}
render() {
return (
<Fragment>
<Container component="main" maxWidth="xs">
<div className={this.props.classes.paper}>
<Typography component="h1" variant="h5">
Hello, {this.props.userInfo ? this.props.userInfo.givenName : 'fuck you'}!
</Typography>
<Typography variant="overline">
your assignments
<Tooltip title="Add a new assignment." aria-label="add a new assignment">
<AddIcon
style={{ marginLeft: 8, position: 'relative', top: 1 }}
fontSize="inherit"
onClick={() => this.setState({
selected: undefined,
editing: true
})}
/>
</Tooltip>
</Typography>
<Typography variant="overline"
style={{ fontSize: '0.6em' }}
onClick={() => this.state.syncing || this.sync()}>
{ this.state.syncing ?
'syncing...' : 'synced ' + moment(this.state.lastSync).fromNow()
}
</Typography>
<List>
{ this.state.tasks.map(x =>
<Task
key={x.id}
active={this.state.selected === x.id}
select={this.selectTask}
edit={this.editTask}
delete={this.deleteTask}
{...x}
/>
) }
</List>
</div>
</Container>
<ModifyDialog
open={this.state.editing}
selected={this.state.selected}
handleClose={() =>
this.setState({
editing: false
})
}
/>
<Dialog
open={this.state.confirmDelete}
onClose={() => this.setState({ confirmDelete: false })}
>
<DialogTitle>Delete assignment?</DialogTitle>
<DialogActions>
<Button onClick={() => this.setState({ confirmDelete: false })} color="primary">
Cancel
</Button>
<Button onClick={
() => {
this.setState({ confirmDelete: false })
let db = this.props.database;
db && db.delete(this.state.tasks.find(v => v.id === this.state.selected) as any);
}
} color="primary" autoFocus>
Yes
</Button>
</DialogActions>
</Dialog>
</Fragment>
);
}
})
\ No newline at end of file
import React, { useState } from 'react';
import { Container, CssBaseline, Avatar, Typography, TextField, Button, Tooltip, Box, Link, Snackbar, IconButton } from '@material-ui/core';
import React, { useState, memo } from 'react';
import { Container, CssBaseline, Avatar, Typography, TextField, Button, Tooltip, Box, Link, Snackbar, IconButton, makeStyles } from '@material-ui/core';
// @ts-ignore
import { Online, Offline } from 'react-detect-offline';
import BookIcon from '@material-ui/icons/Book';
import HelpOutlineIcon from '@material-ui/icons/HelpOutline';
import CloseIcon from '@material-ui/icons/Close';
import loginWithGoogle from './lwg.svg';
import { useStyles } from '../old_code/Loader';
const ErrorMap = {
db_fail: 'There was a problem with the database.',
......@@ -15,6 +17,33 @@ const ErrorMap = {
access_denied: 'You did not allow the app access.'
};
const useStyles = makeStyles(theme => (
{
'@global': {
body: {
backgroundColor: theme.palette.common.white,
},
},
paper: {
marginTop: theme.spacing(8),
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main,
},
form: {
width: '100%',
marginTop: theme.spacing(1),
},
submit: {
margin: theme.spacing(3, 0, 2),
},
}
));
export default () => {
const classes = useStyles(),
urlParams = new URLSearchParams(window.location.search),
......@@ -52,52 +81,59 @@ export default () => {
<form className={classes.form} onSubmit={
e => e.preventDefault()
}>
<Typography component="p" variant="overline">
login using school id
<Tooltip title="This could be a short name or numeric id, e.g. 1234." aria-label="this could be a short name or numeric id, for example 1234">
<HelpOutlineIcon fontSize="inherit" style={{ marginLeft: 8, position: 'relative', top: 1 }} />
</Tooltip>
</Typography>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="username"
label="Username or ID"
name="username"
autoComplete="username"
autoFocus
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
Sign In
</Button>
<Typography component="p" variant="overline">
login without id
<Tooltip title="This is available for all users." aria-label="this is available for all users">
<HelpOutlineIcon fontSize="inherit" style={{ marginLeft: 8, position: 'relative', top: 1 }} />
</Tooltip>
</Typography>
<a href="/api/oauth/begin">
<img src={loginWithGoogle} alt="Login with Google" />
</a>
<Online>
<Typography component="p" variant="overline">
login using school id
<Tooltip title="This could be a short name or numeric id, e.g. 1234." aria-label="this could be a short name or numeric id, for example 1234">
<HelpOutlineIcon fontSize="inherit" style={{ marginLeft: 8, position: 'relative', top: 1 }} />
</Tooltip>
</Typography>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="username"
label="Username or ID"
name="username"
autoComplete="username"
autoFocus
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
Sign In
</Button>
<Typography component="p" variant="overline">
login without id
<Tooltip title="This is available for all users." aria-label="this is available for all users">
<HelpOutlineIcon fontSize="inherit" style={{ marginLeft: 8, position: 'relative', top: 1 }} />
</Tooltip>
</Typography>
<a href="/api/oauth/begin">
<img src={loginWithGoogle} alt="Login with Google" />
</a>
</Online>
<Offline>
<Typography component="p">
You are currently offline.
</Typography>
</Offline>
<Typography component="p" variant="overline">
help and information
</Typography>
......
import React from 'react';
import { Container, Typography, makeStyles, LinearProgress } from '@material-ui/core';
const useStyles = makeStyles(theme => (
{
paper: {
marginTop: theme.spacing(16),
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
},
bar: {
marginTop: 12
},
}
));
export default () => {
let classes = useStyles();
return (
<Container component="main" maxWidth="xs">
<div className={classes.paper}>
<Typography component="h1" variant="h4">
Loading Edupack...
<LinearProgress color="secondary" className={classes.bar} />
</Typography>
</div>
</Container>
);
};
\ No newline at end of file
import React, { useState, useContext, useEffect, memo } from 'react';
import { Dialog, Slide, makeStyles, AppBar, Toolbar, IconButton, Typography, DialogContent, Container, TextField, FormControlLabel, Checkbox, DialogActions, Button } from '@material-ui/core';
import { KeyboardDatePicker, MuiPickersUtilsProvider } from '@material-ui/pickers';
import { TransitionProps } from '@material-ui/core/transitions/transition';
import Autocomplete from '@material-ui/lab/Autocomplete';
import DateFnsUtils from '@date-io/date-fns';
import CloseIcon from '@material-ui/icons/Close';
import { AppStateContext } from '../../App';
import { pick } from 'lodash';
import { ulid } from 'ulid';
export interface DialogProps {
open: boolean,
selected?: string,
handleClose: () => void,
}
const useStyles = makeStyles(theme => (
{
appBar: {
position: 'relative',
backgroundColor: theme.palette.grey[800],
},
title: {
marginLeft: theme.spacing(2),
flex: 1,
},
}
));
const Transition = React.forwardRef<unknown, TransitionProps>(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />;
});
const defaults = {
subject: '',
title: '',
description: '',
class: '',
dueDate: null
};
export default memo((props: DialogProps) => {
let classes = useStyles();
const { database } = useContext(AppStateContext),
[ data, setData ] = useState<
{
subject: string,
title: string,
description: string,
class: string,
dueDate: Date | null
}
>(defaults);
useEffect(() => {
if (typeof props.selected === 'undefined'
|| typeof database === 'undefined') {
setData(defaults);
} else {
database.db.entries
.where('id')
.equals(props.selected)
.toArray()
.then(arr =>
arr[0] ?
setData({
...pick(arr[0], [ 'subject', 'title', 'description', 'class' ]),
dueDate: arr[0].dueDate ? new Date(arr[0].dueDate) : undefined
} as any)
: setData(defaults)
)
}
}, [ props.open, props.selected, database ]);
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<Dialog
open={props.open}
onClose={props.handleClose}
TransitionComponent={Transition}
fullWidth>
<AppBar className={classes.appBar}>
<Toolbar>
<IconButton edge="start" color="inherit" onClick={props.handleClose} aria-label="close">
<CloseIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
Edit an assignment
</Typography>
</Toolbar>
</AppBar>
<DialogContent>
<Container fixed>
<Autocomplete
freeSolo
value={data.subject}
onChange={(e, subject) => setData({ ...data, subject })}
options={[ 'english', 'mathematics', 'biology', 'physics', 'chemistry', 'computer science', 'history', 'geography', 'economics', 'psychology', 'religious studies', 'art', 'music', 'drama', 'textiles', 'food tech', 'product design' ]}
renderInput={params =>
<TextField {...params}
label="Subject"
margin="normal"
required
fullWidth
onChange={e => setData({ ...data, subject: e.currentTarget.value })}
/>
}
/>
<TextField
margin="normal"
required
label="Title"
autoComplete="title"
autoFocus
fullWidth
value={data.title}
onChange={e => setData({ ...data, title: e.currentTarget.value })}
/>
<TextField
margin="normal"
label="Description"
fullWidth
value={data.description}
onChange={e => setData({ ...data, description: e.currentTarget.value })}
/>
<TextField
margin="normal"
id="class"
label="Class or teacher"
autoComplete="class"
fullWidth
value={data.class}
onChange={e => setData({ ...data, class: e.currentTarget.value })}
/>
<FormControlLabel
control={
<Checkbox
checked={data.dueDate !== null}
onChange={v =>
setData({
...data,
dueDate: v.currentTarget.checked ? new Date() : null
})
}
/>
}
label="Include due date"
/>
{
data.dueDate !== null &&
<KeyboardDatePicker
margin="normal"
label="Due date"
format="dd/MM/yyyy"
value={data.dueDate}
onChange={dueDate => setData({ ...data, dueDate })}
fullWidth
/>
}
</Container>
</DialogContent>
<DialogActions>
<Button onClick={props.handleClose} color="inherit">
Cancel
</Button>
<Button onClick={
() => {
const { subject, title, description, class: clas, dueDate } = data;
props.handleClose();
if (database) {
database.update({
id: 'local_' + ulid(),
nonce: ulid(),
updatedAt: new Date().toUTCString(),
type: 0,
subject,
title,
description,
class: clas,
dueDate: dueDate ? dueDate.toUTCString() : null,
complete: false
});
}
}
} color="primary">
Save
</Button>
</DialogActions>
</Dialog>
</MuiPickersUtilsProvider>
);
})
......@@ -40,7 +40,7 @@ export function register(config?: Config) {
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
const swUrl = `https://edupack.insrt.uk/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
......
......@@ -10,14 +10,16 @@
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"strict": true,
"strictPropertyInitialization": false,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
"jsx": "react",
"downlevelIteration": true
},
"include": [
"src"
......
......@@ -1410,6 +1410,11 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==
"@types/[email protected]^4.14.146":
version "4.14.146"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.146.tgz#de0d2c8610012f12a6a796455054cbc654f8fecf"
integrity sha512-JzJcmQ/ikHSv7pbvrVNKJU5j9jL9VLf3/gqs048CEnBVVVEv4kve3vLxoPHGvclutS+Il4SBIuQQ087m1eHffw==
"@types/[email protected]":
version "12.12.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.5.tgz#66103d2eddc543d44a04394abb7be52506d7f290"
......@@ -3504,6 +3509,11 @@ [email protected]:
address "^1.0.1"
debug "^2.6.0"
[email protected]^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/dexie/-/dexie-2.0.4.tgz#6027a5e05879424e8f9979d8c14e7420f27e3a11"
integrity sha512-aQ/s1U2wHxwBKRrt2Z/mwFNHMQWhESerFsMYzE+5P5OsIe5o1kgpFMWkzKTtkvkyyEni6mWr/T4HUJuY9xIHLA==
[email protected]^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
......@@ -8586,6 +8596,11 @@ [email protected]^1.0.4:
regenerator-runtime "0.13.3"
whatwg-fetch "3.0.0"
[email protected]^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/react-detect-offline/-/react-detect-offline-2.4.0.tgz#e40f5a588ffb0680bcbcfa33e4d9d39ad805c3ff"
integrity sha512-OHglQ8bpbM4x8m23RhRCm1e1cYtuCoLQ7NKHsC0zzm1z2vQIrNTq7MvhZTQ7TsbgK1XapMewLxGW9QEW3DXC9A==
[email protected]^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-9.1.0.tgz#3ad2bb8848a32319d760d0a84c56c14bdaae5e81"
......@@ -10220,6 +10235,11 @@ [email protected]^3.1.4:
commander "~2.20.0"
source-map "~0.6.1"
[email protected]^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/ulid/-/ulid-2.3.0.tgz#93063522771a9774121a84d126ecd3eb9804071f"
integrity sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==
[email protected]^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
......