Commit f0f92c43 authored by Dustin Eckhardt's avatar Dustin Eckhardt
Browse files

Merge branch 'Branch_9690262d' into 'master'

v1.0.3

See merge request !3
parents 761b663d 8145b499
Pipeline #122670512 passed with stages
in 8 minutes and 50 seconds
1.0.3 - 03.03.2020
------------------
- **Issue** #14: A notification will be shown whenever a newer version is available, allowing to update immediately instead of on next session
- **Issue** #17: Added labels to the grid, that show the months and days. Additionally, the current day is now highlighted until a mood is selected
- The color of each mood is now displayed in the select list, to add some visual aid when selecting the mood on a day
- Slightly improved performance by reducing the amount of unnecessary re-renders after applying a mood to a day
- Added app icons for Apple devices
1.0.2 - 13.11.2019
------------------
- The current version number is now displayed in the menu
......
{
"name": "pixel-diary",
"version": "1.0.2",
"version": "1.0.3",
"private": true,
"dependencies": {
"@bugsnag/browser": "^6.4.1",
......
......@@ -2,12 +2,16 @@
<html lang="en">
<head>
<title>Pixel Diary</title>
<meta name="description" content="A diary for your moods - create a colorful & pixelated overview of your year, by logging your general mood for each day of the year">
<meta charset="utf-8" />
<link rel="shortcut icon" type="image/png" href="%PUBLIC_URL%/favicon.png" sizes="any" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#2C387E" />
<meta http-equiv="cache-control" content="must-revalidate, max-age=31557600" />
<link rel="shortcut icon" type="image/png" href="%PUBLIC_URL%/favicon.png" sizes="any" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/appicon-ios.png">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
......
......@@ -3,7 +3,7 @@
"name": "Pixel Diary",
"icons": [
{
"src": "diary-favicon.png",
"src": "appicon.png",
"sizes": "512x512",
"type": "image/png"
}
......
User-Agent: *
Allow: /$
Disallow: /
\ No newline at end of file
import { AppBar, Button, CircularProgress, Toolbar, Typography } from '@material-ui/core';
import { Warning } from '@material-ui/icons';
import { AppBar, Button, CircularProgress, IconButton, Toolbar, Typography } from '@material-ui/core';
import { Close, Warning } from '@material-ui/icons';
import { OptionsObject, withSnackbar, WithSnackbarProps } from 'notistack';
import React, { useEffect, useState } from 'react';
import React, { Fragment, memo, useEffect, useState } from 'react';
import { bugsnagClient } from '../../helper/bugsnag';
import DataService from '../../services/data-service';
import StorageHandler from '../../storage/storage-handler';
......@@ -18,11 +18,14 @@ interface AppProps {
/** Information about the repository the source code is stored in */
repository: {url: string, name: string, logoSrc: string};
/** Will be set when an update is available - calling it will trigger the update */
update?: () => void;
}
const App: React.FC<AppProps & WithSnackbarProps> = (
{ name, repository, enqueueSnackbar, closeSnackbar },
{ name, repository, update, enqueueSnackbar, closeSnackbar },
) => {
/** Which year is currently being displayed */
const [displayYear, setDisplayYear] = useState<number>(new Date().getFullYear());
......@@ -45,7 +48,7 @@ const App: React.FC<AppProps & WithSnackbarProps> = (
const infoSnackbarOpt: OptionsObject = {
variant: 'info',
persist: true,
action: key => <Button onClick={() => { closeSnackbar(key); }}>Ok</Button>,
action: key => <Button onClick={() => closeSnackbar(key)}>Ok</Button>,
};
/**
......@@ -151,6 +154,23 @@ const App: React.FC<AppProps & WithSnackbarProps> = (
}
};
/**
* Ask the user whether he wants to update now, if an update callback has been provided
*/
useEffect(() => {
if (!update) return;
enqueueSnackbar('A new version is available!', {
variant: 'info',
persist: true,
action: key => (
<Fragment>
<Button onClick={update}>UPDATE</Button>
<IconButton onClick={() => closeSnackbar(key)}><Close /></IconButton>
</Fragment>
),
});
}, [update]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => { init(); }, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
......@@ -170,6 +190,7 @@ const App: React.FC<AppProps & WithSnackbarProps> = (
setDisplayYear={loadYear}
setProgress={setProgress}
setEncrypting={s => { updateStatus('loading', s); updateStatus('encrypting', s); }}
update={update}
/>
</Toolbar>
</AppBar>
......@@ -204,4 +225,4 @@ const App: React.FC<AppProps & WithSnackbarProps> = (
);
};
export default withSnackbar(App);
export default withSnackbar(memo(App));
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Menu, MenuItem } from '@material-ui/core';
import { ArrowBackIos, ArrowForwardIos, Lock, MoreVert, Security } from '@material-ui/icons';
import React, { createElement, useState } from 'react';
import { Badge, Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Menu, MenuItem } from '@material-ui/core';
import { ArrowBackIos, ArrowForwardIos, Lock, MoreVert, NewReleases, Security } from '@material-ui/icons';
import React, { createElement, memo, useState } from 'react';
import { version } from '../../../../package.json';
import privacyInfo from '../../../privacy';
import CryptoService from '../../../services/crypto-service';
......@@ -32,6 +32,9 @@ interface AppMenuProps {
/** Called when the user enabled encryption and the progress is updated */
setProgress: (value: number) => void;
/** Will be set when an update is available - calling it will trigger the update */
update?: () => void;
}
......@@ -39,7 +42,7 @@ interface AppMenuProps {
* Popup menu that contains general app options
*/
const AppMenu: React.FC<AppMenuProps> = (
{ repository, displayYear, disabled, setDisplayYear, setEncrypting, setProgress },
{ repository, displayYear, disabled, setDisplayYear, setEncrypting, setProgress, update },
) => {
/** Anchor element for the popup menu */
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
......@@ -81,7 +84,9 @@ const AppMenu: React.FC<AppMenuProps> = (
return (
<div>
<IconButton id="app-menu" color="inherit" onClick={event => setAnchor(event.currentTarget)}>
<MoreVert />
<Badge invisible={!update} variant="dot" color="secondary">
<MoreVert />
</Badge>
</IconButton>
<Menu
className="menu-popup"
......@@ -120,17 +125,28 @@ const AppMenu: React.FC<AppMenuProps> = (
</MenuItem>
)}
<MenuItem onClick={() => { setAnchor(null); setPrivacyDialog(true); }}>
<MenuItem id="app-menu-privacy" onClick={() => { setAnchor(null); setPrivacyDialog(true); }}>
<Security />
<span>Privacy</span>
</MenuItem>
<MenuItem id="app-menu-privacy" onClick={() => window.open(repository.url, '_blank')}>
<MenuItem onClick={() => window.open(repository.url, '_blank')}>
<div className="menu-icon"><img src={repository.logoSrc} alt="Tanuki" /></div>
<span>
{`Source on ${repository.name}`}
<span className="sup">{`version ${version}`}</span>
</span>
</MenuItem>
{update && (
<MenuItem onClick={update}>
<NewReleases />
<span>
{'Update Available'}
<span className="sup">click to update now</span>
</span>
</MenuItem>
)}
</Menu>
<Dialog open={privacyDialog} onClose={closePrivacyDialog}>
......@@ -149,4 +165,4 @@ const AppMenu: React.FC<AppMenuProps> = (
);
};
export default AppMenu;
export default memo(AppMenu);
import { CircularProgress, IconButton, Menu, MenuItem, Tooltip } from '@material-ui/core';
import { CloudDone, CloudOff, CloudQueue } from '@material-ui/icons';
import React, { Fragment, useState } from 'react';
import React, { Fragment, memo, useState } from 'react';
import CryptoService from '../../../services/crypto-service';
import DataService from '../../../services/data-service';
import StorageHandler from '../../../storage/storage-handler';
......@@ -95,4 +95,4 @@ const CloudMenu: React.FC<CloudMenuProps> = ({ saving, onDisconnect }) => {
);
};
export default CloudMenu;
export default memo(CloudMenu);
......@@ -4,24 +4,62 @@
align-items: center;
cursor: pointer;
background: #37474f;
}
.day.filler {
cursor: not-allowed;
background: repeating-linear-gradient(-45deg, #37474f, #37474f 3px, #282c34 3px, #282c34 6px)
}
&.filler {
cursor: not-allowed;
background: repeating-linear-gradient(-45deg, #37474f, #37474f 3px, #282c34 3px, #282c34 6px)
}
&.blink {
animation: blinking 2s linear infinite;
}
.note-indicator {
display: none;
background-color: white;
width: 10px;
height: 10px;
border-radius: 10px;
position: absolute;
}
.label {
position: absolute;
color: white;
animation: fadeout 3s ease-in 1 forwards;
}
.note-indicator {
display: none;
background-color: white;
width: 10px;
height: 10px;
border-radius: 10px;
position: absolute;
@media screen and (orientation: landscape) {
.label {
font-size: 1.5vmin;
}
}
@media screen and (orientation: portrait) {
.label {
font-size: 2vmin;
}
}
@keyframes blinking {
50% {
background-color: white;
}
}
@keyframes fadeout {
100% {
opacity: 0;
}
}
}
.year:hover {
.note-indicator {
.day .note-indicator:only-child { // only displayed if the pixel is not labeled
display: block;
}
.day .label {
animation: fadeout 0s linear 1 reverse;
}
}
import classNames from 'classnames';
import React, { Fragment, useState } from 'react';
import React, { Fragment, memo, useState } from 'react';
import styled from 'styled-components';
import { isNumber } from 'util';
import DataService from '../../services/data-service';
......@@ -29,9 +29,13 @@ const Day: React.FC<DayProps> = ({ date, isFiller, onUpdate }) => {
/** Controls whether the details dialog is displayed or not */
const [showDetails, setShowDetails] = useState<boolean>(false);
/** Whether this day is set in the future */
const isFuture = date.getTime() > new Date().getTime();
const classes = classNames({
day: true,
filler: isFiller,
filler: isFiller || isFuture,
blink: data.mood === undefined && date.toDateString() === new Date().toDateString(),
});
// set the color of this pixel, depending on the mood
......@@ -54,8 +58,19 @@ const Day: React.FC<DayProps> = ({ date, isFiller, onUpdate }) => {
return (
<Fragment>
<Pixel className={classes} onClick={!isFiller ? () => setShowDetails(true) : undefined}>
<Pixel
className={classes}
onClick={!isFiller && !isFuture ? () => setShowDetails(true) : undefined}
>
{data.note && !isFiller && <span className="note-indicator"></span>}
{!date.getMonth() && !(date.getDate() % 5) && !isFiller
&& <span className="label">{date.getDate()}</span>
}
{date.getDate() === 1 && !(date.getMonth() % 2) && !isFiller
&& <span className="label">{date.toLocaleString('en-US', { month: 'short' })}</span>
}
</Pixel>
{showDetails && ( // if the details should be displayed
......@@ -65,4 +80,4 @@ const Day: React.FC<DayProps> = ({ date, isFiller, onUpdate }) => {
);
};
export default Day;
export default memo(Day);
......@@ -3,5 +3,21 @@
display: grid;
grid-template-columns: 10% 88%;
grid-gap: 2%;
.MuiSelect-root div {
display: none; // hide the mood color inside the select field
}
}
}
#menu-mood {
.mood-menu-item {
display: grid;
grid-template-columns: 15px auto;
grid-gap: 10px;
}
.MuiPopover-paper {
margin-left: -24px
}
}
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, FilledInput, FormControl, InputLabel, MenuItem, Select, TextField } from '@material-ui/core';
import React, { useState } from 'react';
import React, { memo, useState } from 'react';
import styled from 'styled-components';
import IDay from '../../../types/day';
import Moods from '../../../types/moods';
......@@ -72,7 +72,19 @@ const DayDetails: React.FC<DayDetailsProps> = ({ date, values, onClose }) => {
onChange={handleChange}
input={<FilledInput name="mood" id="mood-select" />}
>
{Moods.map((v, i) => <MenuItem key={v.color} value={i}>{v.name}</MenuItem>)}
{Moods.map((v, i) => {
const MoodColor = styled.div`
background-color: ${v.color};
height: 80%;
`;
return (
<MenuItem className="mood-menu-item" key={v.color} value={i}>
<MoodColor />
<span>{v.name}</span>
</MenuItem>
);
})}
</Select>
</FormControl>
</div>
......@@ -98,4 +110,4 @@ const DayDetails: React.FC<DayDetailsProps> = ({ date, values, onClose }) => {
);
};
export default DayDetails;
export default memo(DayDetails);
import { FormHelperText, IconButton, InputAdornment, TextField } from '@material-ui/core';
import { ArrowForward, Lock } from '@material-ui/icons';
import React, { useState } from 'react';
import React, { memo, useState } from 'react';
import CryptoService from '../../services/crypto-service';
import './lockscreen.scss';
......@@ -64,4 +64,4 @@ const Lockscreen: React.FC<LockscreenProps> = ({ checkCipher, onUnlock }) => {
);
};
export default Lockscreen;
export default memo(Lockscreen);
import React from 'react';
import React, { memo } from 'react';
import Day from '../day/day';
import './month.scss';
......@@ -25,7 +25,7 @@ const Month: React.FC<MonthProps> = ({ date, onDayUpdated }) => {
key={`${year}-${month}-${i}`}
date={new Date(year, month, i)}
onUpdate={onDayUpdated}
isFiller={i > daysInMonth() || new Date(year, month, i).getTime() > new Date().getTime()}
isFiller={i > daysInMonth()}
/>,
);
}
......@@ -33,4 +33,4 @@ const Month: React.FC<MonthProps> = ({ date, onDayUpdated }) => {
return <div className="month">{renderDays}</div>;
};
export default Month;
export default memo(Month);
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, FormHelperText, IconButton, InputAdornment, TextField } from '@material-ui/core';
import { Lock, Visibility, VisibilityOff } from '@material-ui/icons';
import React, { useState } from 'react';
import React, { memo, useState } from 'react';
import './password-input.scss';
interface PasswordInputProps {
......@@ -72,4 +72,4 @@ const PasswordInput: React.FC<PasswordInputProps> = ({ onClose }) => {
);
};
export default PasswordInput;
export default memo(PasswordInput);
import React from 'react';
import React, { memo } from 'react';
import Month from '../month/month';
import './year.scss';
......@@ -29,4 +29,4 @@ const Year: React.FC<YearProps> = ({ year, onDayUpdated }) => {
return <div className="year">{renderMonths}</div>;
};
export default Year;
export default memo(Year);
import { SnackbarProvider } from 'notistack';
import React from 'react';
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { version } from '../package.json';
import gitlabLogo from './assets/gitlab.svg';
......@@ -8,21 +8,31 @@ import Bugsnag from './helper/bugsnag';
import './index.scss';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<Bugsnag>
<SnackbarProvider maxSnack={2} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}>
<App
name="Pixel Diary"
repository={{
name: 'GitLab',
url: 'https://gitlab.com/eggerd/pixel-diary',
logoSrc: gitlabLogo,
}}
/>
</SnackbarProvider>
</Bugsnag>,
document.getElementById('root'),
);
const Root: React.FC = () => {
/** Will only be set when an update is available */
const [workerReg, setWorkerReg] = useState<ServiceWorkerRegistration | undefined>(undefined);
serviceWorker.register();
useEffect(() => {
serviceWorker.register({ onUpdate: (reg: ServiceWorkerRegistration) => setWorkerReg(reg) });
}, []);
return (
<Bugsnag>
<SnackbarProvider maxSnack={2} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}>
<App
name="Pixel Diary"
repository={{
name: 'GitLab',
url: 'https://gitlab.com/eggerd/pixel-diary',
logoSrc: gitlabLogo,
}}
// unregister service worker and reload page to apply the update
update={workerReg && (() => workerReg.unregister().then(() => window.location.reload()))}
/>
</SnackbarProvider>
</Bugsnag>
);
};
ReactDOM.render(<Root />, document.getElementById('root'));
console.log(`Version ${version} (${process.env.REACT_APP_ENVIRONMENT || process.env.NODE_ENV})`);
......@@ -6,7 +6,7 @@ const privacyInfo = (
<p>
This website does not use cookies and does not collect any kind of personal information about
its visitors. All information entered on this website, for example a mood or a note (hereafter
called "Diary Content"), are stored in the integrated storage of the visitors browser, unless
called "Diary Content"), is stored in the integrated storage of the visitors browser, unless
the visitor chooses to connect a cloud storage (see "Cloud Storage").
</p>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment