Merge pull request #66 from GRA0007/dev

Update dialog
This commit is contained in:
Benjamin Grant 2021-06-17 00:40:12 +10:00 committed by GitHub
commit 172bfeefa1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 167 additions and 261 deletions

View file

@ -1,3 +1,16 @@
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const path = require('path');
// const fs = require('fs');
// const glob = require('glob');
// const crypto = require('crypto');
// const fileHash = path => {
// let file_buffer = fs.readFileSync(path);
// let sum = crypto.createHash('md5');
// sum.update(file_buffer);
// return sum.digest('hex');
// };
module.exports = function override(config, env) {
config.output.filename = env === 'production'
? 'static/js/[name].[contenthash].js'
@ -7,5 +20,29 @@ module.exports = function override(config, env) {
? 'static/js/[name].[contenthash].chunk.js'
: env === 'development' && 'static/js/[name].chunk.js';
if (env === 'production') {
config.plugins.push(new WorkboxWebpackPlugin.InjectManifest({
swSrc: path.resolve(__dirname, 'src/sw.js'),
exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
additionalManifestEntries: [
// ...glob.sync('./public/i18n/*/*.json').map(file => {
// return ({
// url: file.replace('./public', ''),
// revision: fileHash(file),
// });
// }),
{
url: '/index.css',
revision: null,
},
{
url: '/manifest.json',
revision: null,
},
],
}));
}
return config;
}

View file

@ -40,6 +40,7 @@
"workbox-routing": "^5.1.3",
"workbox-strategies": "^5.1.3",
"workbox-streams": "^5.1.3",
"workbox-window": "^6.1.5",
"zustand": "^3.3.2"
},
"scripts": {
@ -67,6 +68,7 @@
]
},
"devDependencies": {
"react-app-rewired": "^2.1.8"
"react-app-rewired": "^2.1.8",
"workbox-webpack-plugin": "^5.1.3"
}
}

View file

@ -54,5 +54,13 @@
"language": {
"label": "Language"
}
},
"update": {
"heading": "Crab Fit has been updated",
"body": "A new version of Crab Fit is available, which includes updates, fixes, and new features.",
"buttons": {
"close": "Close",
"reload": "Reload"
}
}
}

View file

@ -1,23 +0,0 @@
{
"name": "How to Crab Fit",
"p1": "Crab Fit is a tool that helps you when planning events with friends or coworkers. You just create an event, enter your availability, send it out, and see when everyone is free!",
"p2": "See below for detailed steps of how to Crab Fit your event.",
"s1": "Step 1",
"p3": "Use the form at <1>crab.fit</1> to make a new event. You only need to put in the rough time period for when your event occurs here, not your availability.",
"p4": "For example, we'll use \"Jenny's Birthday Lunch\". Jenny wants her birthday lunch to happen on the same week as her birthday, the 15th of April, but she knows that not all of her friends are available on the 15th. She also doesn't want to do it on the weekend.",
"p5": "Jenny also knows that since it's a lunch event, it can't start before 11am or go any later than 5pm.",
"s2": "Step 2",
"p6": "Enter your availability for the event you just created.",
"p7": "In our example, Jenny now puts in her availability for her birthday lunch. She is free all week, except after 3pm on Tuesday and Wednesday, and before 1pm on Friday.",
"s3": "Step 3",
"p8": "Send the link to everyone you want to come.",
"p9": "After Jenny has sent the link to her friends and waited for them to also fill out their availabilities, she can now easily see them all on the heatmap below and choose the darkest area for a time that suits everyone!",
"p10": "In this example, 1pm to 3pm on Friday the 16th works for all Jenny's friends."
}

View file

@ -1,57 +0,0 @@
{
"create": "CREATE A",
"recently_visited": "Recently visited",
"nav": {
"about": "About",
"donate": "Donate"
},
"form": {
"name": {
"label": "Give your event a name!",
"sublabel": "Or leave blank to generate one"
},
"dates": {
"label": "What dates might work?",
"sublabel": "Click and drag to select",
"options": {
"specific": "Specific dates",
"week": "Days of the week"
},
"tooltips": {
"previous": "先月",
"next": "来月",
"today": "今日"
}
},
"times": {
"label": "What times might work?",
"sublabel": "Click and drag to select a time range"
},
"timezone": {
"label": "And the timezone",
"defaultOption": "Select..."
},
"button": "Create",
"errors": {
"no_dates": "There aren't any dates selected",
"same_times": "The start and end times can't be the same",
"no_time": "There isn't any time selected",
"unknown": "Something went wrong. Please try again later."
}
},
"offline": "You can't create a Crab Fit when you don't have an internet connection. Please make sure you're connected.",
"about": {
"name": "About Crab Fit",
"events": "Events created",
"availabilities": "Availabilities entered",
"content": {
"p1": "Crab Fit helps you fit your event around everyone's schedules. Simply create an event above and send the link to everyone that is participating. Results update live and you will be able to see a heat-map of when everyone is free.<1/><2>Learn more about how to Crab Fit</2>.",
"p2": "Create a lot of Crab Fits? Get the <1>Chrome extension</1> or <3>Firefox extension</3> for your browser! You can also download the <5>Android app</5> to Crab Fit on the go.",
"p3": "Created by <1>Ben Grant</1>, Crab Fit is the modern-day solution to your group event planning debates.",
"p4": "The code for Crab Fit is open source, if you find any issues or want to contribute, you can visit the <1>repository</1>. By using Crab Fit you agree to the <3>privacy policy</3>.",
"p5": "Consider donating below if it helped you out so it can stay free for everyone. 🦀"
}
}
}

View file

@ -1,8 +1,9 @@
import { useState, useEffect, useCallback, Suspense, lazy } from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import { ThemeProvider, Global } from '@emotion/react';
import { Workbox } from 'workbox-window';
import { Settings, Loading, Egg } from 'components';
import { Settings, Loading, Egg, UpdateDialog } from 'components';
import { useSettingsStore } from 'stores';
import theme from 'theme';
@ -15,6 +16,8 @@ const Create = lazy(() => import('pages/Create/Create'));
const Help = lazy(() => import('pages/Help/Help'));
const Privacy = lazy(() => import('pages/Privacy/Privacy'));
const wb = new Workbox('sw.js');
const App = () => {
const colortheme = useSettingsStore(state => state.theme);
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
@ -25,6 +28,8 @@ const App = () => {
const [eggVisible, setEggVisible] = useState(false);
const [eggKey, setEggKey] = useState(0);
const [updateAvailable, setUpdateAvailable] = useState(false);
const eggHandler = useCallback(
event => {
if (EGG_PATTERN.indexOf(event.key) < 0 || event.key !== EGG_PATTERN[eggCount]) {
@ -56,6 +61,19 @@ const App = () => {
};
}, []);
useEffect(() => {
// Register service worker
if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') {
wb.addEventListener('installed', event => {
if (event.isUpdate) {
setUpdateAvailable(true);
}
});
wb.register();
}
}, []);
useEffect(() => {
document.addEventListener('keyup', eggHandler, false);
@ -140,6 +158,12 @@ const App = () => {
)} />
</Switch>
{updateAvailable && (
<Suspense fallback={<Loading />}>
<UpdateDialog onClose={() => setUpdateAvailable(false)} />
</Suspense>
)}
{eggVisible && <Egg eggKey={eggKey} onClose={() => setEggVisible(false)} />}
</ThemeProvider>
</BrowserRouter>

View file

@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import { useTheme } from '@emotion/react';
import { useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
@ -25,6 +26,7 @@ const setDefaults = (lang, store) => {
};
const Settings = () => {
const { pathname } = useLocation();
const theme = useTheme();
const store = useSettingsStore();
const [isOpen, _setIsOpen] = useState(false);
@ -71,6 +73,13 @@ const Settings = () => {
setDefaults(lang, store);
});
// Reset scroll on navigation
useEffect(() => {
document.documentElement.style.scrollBehavior = 'auto';
window.scrollTo(0, 0);
document.documentElement.style.scrollBehavior = 'smooth';
}, [pathname]);
return (
<>
<OpenButton

View file

@ -0,0 +1,24 @@
import { Button } from 'components';
import { useTranslation } from 'react-i18next';
import {
Wrapper,
ButtonWrapper,
} from './updateDialogStyle';
const UpdateDialog = ({ onClose }) => {
const { t } = useTranslation('common');
return (
<Wrapper>
<h2>{t('common:update.heading')}</h2>
<p>{t('common:update.body')}</p>
<ButtonWrapper>
<Button secondary onClick={onClose}>{t('common:update.buttons.close')}</Button>
<Button onClick={() => window.location.reload()}>{t('common:update.buttons.reload')}</Button>
</ButtonWrapper>
</Wrapper>
);
}
export default UpdateDialog;

View file

@ -0,0 +1,34 @@
import styled from '@emotion/styled';
export const Wrapper = styled.div`
position: fixed;
bottom: 20px;
right: 20px;
background-color: ${props => props.theme.background};
${props => props.theme.mode === 'dark' && `
border: 1px solid ${props.theme.primaryBackground};
`}
z-index: 900;
padding: 20px 26px;
border-radius: 3px;
width: 400px;
box-sizing: border-box;
max-width: calc(100% - 20px);
box-shadow: 0 3px 6px 0 rgba(0,0,0,.3);
& h2 {
margin: 0;
font-size: 1.3rem;
}
& p {
margin: 16px 0 24px;
font-size: 1rem;
}
`;
export const ButtonWrapper = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
gap: 16px;
`;

View file

@ -18,6 +18,7 @@ export { default as Egg } from './Egg/Egg';
export { default as Footer } from './Footer/Footer';
export { default as Recents } from './Recents/Recents';
export { default as Logo } from './Logo/Logo';
export { default as UpdateDialog } from './UpdateDialog/UpdateDialog';
export const _GoogleCalendar = () => import('./GoogleCalendar/GoogleCalendar');
export const _OutlookCalendar = () => import('./OutlookCalendar/OutlookCalendar');

View file

@ -1,8 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
import reportWebVitals from './reportWebVitals';
import 'i18n';
ReactDOM.render(
@ -11,10 +9,3 @@ ReactDOM.render(
</React.StrictMode>,
document.getElementById('root')
);
serviceWorkerRegistration.register();
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View file

@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
@ -22,11 +22,14 @@ const Privacy = () => {
const { push } = useHistory();
const { t, i18n } = useTranslation(['common', 'privacy']);
const contentRef = useRef();
const [content, setContent] = useState('');
useEffect(() => {
document.title = `${t('privacy:name')} - Crab Fit`;
}, [t]);
useEffect(() => setContent(contentRef.current?.innerText || ''), [contentRef]);
return (
<>
<StyledMain>
@ -39,7 +42,7 @@ const Privacy = () => {
{!i18n.language.startsWith('en') && (
<p>
<a
href={`https://translate.google.com/?sl=en&tl=${i18n.language.substring(0, 2)}&text=${encodeURIComponent(`${translationDisclaimer}\n\n${contentRef.current?.innerText}`)}&op=translate`}
href={`https://translate.google.com/?sl=en&tl=${i18n.language.substring(0, 2)}&text=${encodeURIComponent(`${translationDisclaimer}\n\n${content}`)}&op=translate`}
target="_blank"
rel="noreferrer noopener"
>{t('privacy:translate')}</a>

View file

@ -1,137 +0,0 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://cra.link/PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://cra.link/PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://cra.link/PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch((error) => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' },
})
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log('No internet connection found. App is running in offline mode.');
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
registration.unregister();
})
.catch((error) => {
console.error(error.message);
});
}
}

View file

@ -1,24 +1,15 @@
/* eslint-disable no-restricted-globals */
// This service worker can be customized!
// See https://developers.google.com/web/tools/workbox/modules
// for the list of available Workbox modules, or add any other
// code you'd like.
// You can also remove this file if you'd prefer not to use a
// service worker, and the Workbox build step will be skipped.
import { clientsClaim } from 'workbox-core';
import { clientsClaim, skipWaiting } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate, NetworkFirst } from 'workbox-strategies';
skipWaiting();
clientsClaim();
// Precache all of the assets generated by your build process.
// Their URLs are injected into the manifest variable below.
// This variable must be present somewhere in your service worker file,
// even if you decide not to use precaching. See https://cra.link/PWA
// Injection point
precacheAndRoute(self.__WB_MANIFEST);
// Set up App Shell-style routing, so that all navigation requests
@ -46,8 +37,6 @@ registerRoute(
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);
// An example runtime caching route for requests that aren't handled by the
// precache, in this case same-origin .png requests like those from in public/
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) => url.origin === self.location.origin && (
@ -70,21 +59,10 @@ registerRoute(
})
);
// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// Any other custom service worker logic can go here.
registerRoute(
({ url }) => url.origin === self.location.origin && (
url.pathname.endsWith('.js')
|| url.pathname.endsWith('.css')
|| url.pathname.endsWith('.json')
),
new NetworkFirst({ cacheName: 'res' })
// Add in any other file extensions or routing criteria as needed.
({ url }) => url.origin === self.location.origin && url.pathname.includes('i18n'),
new NetworkFirst({
cacheName: 'i18n',
})
);

View file

@ -11522,6 +11522,11 @@ workbox-core@^5.1.3, workbox-core@^5.1.4:
resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-5.1.4.tgz#8bbfb2362ecdff30e25d123c82c79ac65d9264f4"
integrity sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==
workbox-core@^6.1.5:
version "6.1.5"
resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.1.5.tgz#424ff600e2c5448b14ebd58b2f5ac8ed91b73fb9"
integrity sha512-9SOEle7YcJzg3njC0xMSmrPIiFjfsFm9WjwGd5enXmI8Lwk8wLdy63B0nzu5LXoibEmS9k+aWF8EzaKtOWjNSA==
workbox-expiration@^5.1.3, workbox-expiration@^5.1.4:
version "5.1.4"
resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-5.1.4.tgz#92b5df461e8126114943a3b15c55e4ecb920b163"
@ -11588,7 +11593,7 @@ workbox-sw@^5.1.4:
resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-5.1.4.tgz#2bb34c9f7381f90d84cef644816d45150011d3db"
integrity sha512-9xKnKw95aXwSNc8kk8gki4HU0g0W6KXu+xks7wFuC7h0sembFnTrKtckqZxbSod41TDaGh+gWUA5IRXrL0ECRA==
workbox-webpack-plugin@5.1.4:
workbox-webpack-plugin@5.1.4, workbox-webpack-plugin@^5.1.3:
version "5.1.4"
resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-5.1.4.tgz#7bfe8c16e40fe9ed8937080ac7ae9c8bde01e79c"
integrity sha512-PZafF4HpugZndqISi3rZ4ZK4A4DxO8rAqt2FwRptgsDx7NF8TVKP86/huHquUsRjMGQllsNdn4FNl8CD/UvKmQ==
@ -11607,6 +11612,13 @@ workbox-window@^5.1.4:
dependencies:
workbox-core "^5.1.4"
workbox-window@^6.1.5:
version "6.1.5"
resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.1.5.tgz#017b22342e10c6df6b9672326b575ec950b6cd80"
integrity sha512-akL0X6mAegai2yypnq78RgfazeqvKbsllRtEI4dnbhPcRINEY1NmecFmsQk8SD+zWLK1gw5OdwAOX+zHSRVmeA==
dependencies:
workbox-core "^6.1.5"
worker-farm@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"