Merge pull request #72 from ente-io/design-update

Updated design
This commit is contained in:
Pushkar Anand 2021-05-23 20:29:26 +05:30 committed by GitHub
commit 4e09320f8e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1757 additions and 299 deletions

6
.gitignore vendored
View file

@ -38,3 +38,9 @@ out_publish
.env .env
/.vscode/ /.vscode/
# workbox
public/sw.js
public/sw.js.map
public/worker-*.js
public/worker-*.js.map

View file

@ -4,6 +4,7 @@ const WorkerPlugin = require('worker-plugin');
const withBundleAnalyzer = require('@next/bundle-analyzer')({ const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true', enabled: process.env.ANALYZE === 'true',
}); });
const withWorkbox = require("next-with-workbox");
const { const {
NEXT_PUBLIC_SENTRY_DSN: SENTRY_DSN, NEXT_PUBLIC_SENTRY_DSN: SENTRY_DSN,
@ -17,7 +18,7 @@ const {
process.env.SENTRY_DSN = SENTRY_DSN; process.env.SENTRY_DSN = SENTRY_DSN;
const basePath = ''; const basePath = '';
module.exports = withBundleAnalyzer({ module.exports = withWorkbox(withBundleAnalyzer({
target: 'serverless', target: 'serverless',
productionBrowserSourceMaps: true, productionBrowserSourceMaps: true,
env: { env: {
@ -26,6 +27,9 @@ module.exports = withBundleAnalyzer({
// outside of Vercel // outside of Vercel
NEXT_PUBLIC_COMMIT_SHA: COMMIT_SHA, NEXT_PUBLIC_COMMIT_SHA: COMMIT_SHA,
}, },
workbox: {
swSrc: "src/serviceWorker.js",
},
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
if (!isServer) { if (!isServer) {
config.plugins.push( config.plugins.push(
@ -64,4 +68,4 @@ module.exports = withBundleAnalyzer({
} }
return config; return config;
}, },
}); }));

View file

@ -26,6 +26,7 @@
"libsodium-wrappers": "^0.7.8", "libsodium-wrappers": "^0.7.8",
"localforage": "^1.9.0", "localforage": "^1.9.0",
"next": "9.5.3", "next": "9.5.3",
"next-with-workbox": "^2.0.1",
"node-forge": "^0.10.0", "node-forge": "^0.10.0",
"photoswipe": "file:./thirdparty/photoswipe", "photoswipe": "file:./thirdparty/photoswipe",
"react": "16.13.1", "react": "16.13.1",
@ -39,6 +40,11 @@
"react-window-infinite-loader": "^1.0.5", "react-window-infinite-loader": "^1.0.5",
"scrypt-js": "^3.0.1", "scrypt-js": "^3.0.1",
"styled-components": "^5.2.0", "styled-components": "^5.2.0",
"workbox-precaching": "^6.1.5",
"workbox-recipes": "^6.1.5",
"workbox-routing": "^6.1.5",
"workbox-strategies": "^6.1.5",
"workbox-window": "^6.1.5",
"xml-js": "^1.6.11", "xml-js": "^1.6.11",
"yup": "^0.29.3" "yup": "^0.29.3"
}, },

BIN
public/images/ente-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

27
public/manifest.json Normal file
View file

@ -0,0 +1,27 @@
{
"short_name": "ente",
"name": "ente | encrypted photo storage",
"icons": [
{
"src": "/images/ente-192.png",
"type": "image/png",
"sizes": "192x192"
}
],
"start_url": "/",
"background_color": "#191919",
"display": "standalone",
"scope": "/",
"theme_color": "#111",
"description": "ente provides a simple way to back up your memories.",
"related_applications": [
{
"platform": "play",
"url": "https://play.google.com/store/apps/details?id=io.ente.photos",
"id": "io.ente.photos"
}, {
"platform": "itunes",
"url": "https://apps.apple.com/in/app/ente-photos/id1542026904"
}
]
}

9
public/offline.html Normal file
View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>ente.io | encrypted photo storage</title>
</head>
<body>
<h1>You are offline!</h1>
</body>
</html>

View file

@ -0,0 +1,22 @@
import React from 'react';
export default function ArrowEast(props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
{...props}
>
<rect fill="none" height="24" width="24"/>
<path d="M15,5l-1.41,1.41L18.17,11H2V13h16.17l-4.59,4.59L15,19l7-7L15,5z"/>
</svg>
);
}
ArrowEast.defaultProps = {
height: 24,
width: 24,
viewBox: '0 0 24 24',
};

View file

@ -0,0 +1,22 @@
import React from 'react';
export default function CloudUpload(props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
fill="currentColor"
>
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4 0-2.05 1.53-3.76 3.56-3.97l1.07-.11.5-.95C8.08 7.14 9.94 6 12 6c2.62 0 4.88 1.86 5.39 4.43l.3 1.5 1.53.11c1.56.1 2.78 1.41 2.78 2.96 0 1.65-1.35 3-3 3zM8 13h2.55v3h2.9v-3H16l-4-4z"/>
</svg>
);
}
CloudUpload.defaultProps = {
height: 24,
width: 24,
viewBox: '0 0 24 24',
};

View file

@ -0,0 +1,21 @@
import React from 'react';
export default function NavigateNext(props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height="40"
viewBox="0 0 24 24"
width="24px"
fill="currentColor"
>
<path d="M0 0h24v24H0z" fill="none"/><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
</svg>
);
}
NavigateNext.defaultProps = {
height: 24,
width: 24,
viewBox: '0 0 24 24',
};

View file

@ -0,0 +1,62 @@
import React, { useEffect, useLayoutEffect, useState } from 'react';
import styled from 'styled-components';
import NavigateNext from './NavigateNext';
export enum SCROLL_DIRECTION {
LEFT = -1,
RIGHT = +1,
}
interface Props {
scrollDirection: SCROLL_DIRECTION;
}
const Wrapper = styled.button<{ direction: SCROLL_DIRECTION }>`
height: 40px;
width: 40px;
margin-top: 10px;
background-color: #191919;
border: none;
color: #eee;
z-index: 1;
position: absolute;
${(props) => props.direction === SCROLL_DIRECTION.LEFT ? 'margin-right: 10px;' : 'margin-left: 10px;'}
${(props) => props.direction === SCROLL_DIRECTION.LEFT ? 'left: 0;' : 'right: 0;'}
& > svg {
${(props) =>props.direction === SCROLL_DIRECTION.LEFT && `transform:rotate(180deg);`}
border-radius: 50%;
height: 30px;
width: 30px;
}
&:hover > svg {
background-color: #555;
}
&:hover {
color:#fff;
}
&::after {
content: ' ';
background: linear-gradient(to ${(props) => props.direction === SCROLL_DIRECTION.LEFT ? 'right' : 'left'}, #191919 5%, rgba(255, 255, 255, 0) 80%);
position: absolute;
top: 0;
width: 40px;
height: 40px;
${(props) => props.direction === SCROLL_DIRECTION.LEFT ? 'left: 40px;' : 'right: 40px;'}
}
`;
const NavigationButton = ({ scrollDirection, ...rest }) => {
return (
<Wrapper
direction={scrollDirection}
{...rest}
>
<NavigateNext />
</Wrapper>
);
};
export default NavigationButton;

View file

@ -10,6 +10,7 @@ import constants from 'utils/strings/constants';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { VariableSizeList as List } from 'react-window'; import { VariableSizeList as List } from 'react-window';
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe'; import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
import CloudUpload from './CloudUpload';
const DATE_CONTAINER_HEIGHT = 45; const DATE_CONTAINER_HEIGHT = 45;
const IMAGE_CONTAINER_HEIGHT = 200; const IMAGE_CONTAINER_HEIGHT = 200;
@ -70,6 +71,19 @@ const DateContainer = styled.div`
padding-top: 15px; padding-top: 15px;
`; `;
const EmptyScreen = styled.div`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
flex: 1;
color: #2dc262;
& > svg {
filter: drop-shadow(3px 3px 5px rgba(45,194,98,0.5));
}
`;
enum ITEM_TYPE { enum ITEM_TYPE {
TIME = 'TIME', TIME = 'TIME',
TILE = 'TILE', TILE = 'TILE',
@ -261,13 +275,8 @@ const PhotoFrame = ({
return ( return (
<> <>
{!isFirstLoad && files.length == 0 ? ( {!isFirstLoad && files.length == 0 ? (
<div <EmptyScreen>
style={{ <CloudUpload width={150} height={150} />
height: '60%',
display: 'grid',
placeItems: 'center',
}}
>
<Button <Button
variant="outline-success" variant="outline-success"
onClick={openFileUploader} onClick={openFileUploader}
@ -280,7 +289,7 @@ const PhotoFrame = ({
> >
{constants.UPLOAD_FIRST_PHOTO} {constants.UPLOAD_FIRST_PHOTO}
</Button> </Button>
</div> </EmptyScreen>
) : filteredData.length ? ( ) : filteredData.length ? (
<Container> <Container>
<AutoSizer> <AutoSizer>

View file

@ -65,6 +65,15 @@ function PhotoSwipe(props: Iprops) {
maxSpreadZoom: 5, maxSpreadZoom: 5,
index: currentIndex, index: currentIndex,
showHideOpacity: true, showHideOpacity: true,
getDoubleTapZoom: function(isMouseClick, item) {
if(isMouseClick) {
return 2.5;
} else {
// zoom to original if initial zoom is less than 0.7x,
// otherwise to 1.5x, to make sure that double-tap gesture always zooms image
return item.initialZoomLevel < 0.7 ? 1 : 1.5;
}
},
}; };
let photoSwipe = new Photoswipe( let photoSwipe = new Photoswipe(
pswpElement, pswpElement,

View file

@ -104,7 +104,7 @@ export default function Sidebar(props: Props) {
> >
<div <div
style={{ style={{
marginBottom: '28px', marginBottom: '8px',
outline: 'none', outline: 'none',
color: 'rgb(45, 194, 98)', color: 'rgb(45, 194, 98)',
fontSize: '16px', fontSize: '16px',
@ -112,147 +112,141 @@ export default function Sidebar(props: Props) {
> >
{user?.email} {user?.email}
</div> </div>
<div style={{ outline: 'none' }}> <div style={{ flex: 1, overflow: 'auto' }}>
<div style={{ display: 'flex' }}> <div style={{ outline: 'none' }}>
<h5 style={{ margin: '4px 0 12px 2px' }}> <div style={{ display: 'flex' }}>
{constants.SUBSCRIPTION_PLAN} <h5 style={{ margin: '4px 0 12px 2px' }}>
</h5> {constants.SUBSCRIPTION_PLAN}
<div style={{ marginLeft: '10px' }}> </h5>
{
<Button
variant={
isSubscribed(subscription)
? 'outline-secondary'
: 'outline-success'
}
size="sm"
onClick={onManageClick}
>
{isSubscribed(subscription)
? constants.MANAGE
: constants.SUBSCRIBE}
</Button>
}
</div> </div>
</div> <div style={{ color: '#959595' }}>
<div style={{ color: '#959595' }}> {isSubscriptionActive(subscription) ? (
{isSubscriptionActive(subscription) ? ( isOnFreePlan(subscription) ? (
isOnFreePlan(subscription) ? ( constants.FREE_SUBSCRIPTION_INFO(
constants.FREE_SUBSCRIPTION_INFO( subscription?.expiryTime
subscription?.expiryTime )
) ) : isSubscriptionCancelled(subscription) ? (
) : isSubscriptionCancelled(subscription) ? ( constants.RENEWAL_CANCELLED_SUBSCRIPTION_INFO(
constants.RENEWAL_CANCELLED_SUBSCRIPTION_INFO( subscription?.expiryTime
subscription?.expiryTime )
) : (
constants.RENEWAL_ACTIVE_SUBSCRIPTION_INFO(
subscription?.expiryTime
)
) )
) : ( ) : (
constants.RENEWAL_ACTIVE_SUBSCRIPTION_INFO( <p>{constants.SUBSCRIPTION_EXPIRED}</p>
subscription?.expiryTime )}
) <Button
) variant="outline-success"
) : ( block
<p>{constants.SUBSCRIPTION_EXPIRED}</p> size="sm"
)} onClick={onManageClick}
>
{isSubscribed(subscription)
? constants.MANAGE
: constants.SUBSCRIBE}
</Button>
</div>
</div> </div>
</div> <div style={{ outline: 'none', marginTop: '30px' }}></div>
<div style={{ outline: 'none', marginTop: '30px' }}></div> <div>
<div> <h5 style={{ marginBottom: '12px' }}>
<h5 style={{ marginBottom: '12px' }}> {constants.USAGE_DETAILS}
{constants.USAGE_DETAILS} </h5>
</h5> <div style={{ color: '#959595' }}>
<div style={{ color: '#959595' }}> {usage ? (
{usage ? ( constants.USAGE_INFO(
constants.USAGE_INFO( usage,
usage, Math.ceil(
Math.ceil( Number(convertBytesToGBs(subscription?.storage))
Number(convertBytesToGBs(subscription?.storage)) )
) )
) ) : (
) : ( <div style={{ textAlign: 'center' }}>
<div style={{ textAlign: 'center' }}> <EnteSpinner
<EnteSpinner style={{
style={{ borderWidth: '2px',
borderWidth: '2px', width: '20px',
width: '20px', height: '20px',
height: '20px', }}
}} />
/> </div>
</div> )}
)} </div>
</div> </div>
</div> <div
<div style={{
style={{ height: '1px',
height: '1px', marginTop: '40px',
marginTop: '40px', background: '#242424',
background: '#242424', width: '100%',
width: '100%', }}
}} ></div>
></div> <LinkButton style={{ marginTop: '30px' }} onClick={openFeedbackURL}>
<LinkButton style={{ marginTop: '30px' }} onClick={openFeedbackURL}> {constants.REQUEST_FEATURE}
{constants.REQUEST_FEATURE} </LinkButton>
</LinkButton> <LinkButton style={{ marginTop: '30px' }} onClick={openSupportMail}>
<LinkButton style={{ marginTop: '30px' }} onClick={openSupportMail}> {constants.SUPPORT}
{constants.SUPPORT} </LinkButton>
</LinkButton> <>
<> <RecoveryKeyModal
<RecoveryKeyModal show={recoverModalView}
show={recoverModalView} onHide={() => setRecoveryModalView(false)}
onHide={() => setRecoveryModalView(false)} somethingWentWrong={() =>
somethingWentWrong={() => props.setDialogMessage({
props.setDialogMessage({ title: constants.RECOVER_KEY_GENERATION_FAILED,
title: constants.RECOVER_KEY_GENERATION_FAILED, close: { variant: 'danger' },
close: { variant: 'danger' }, })
}) }
} />
/> <LinkButton
style={{ marginTop: '30px' }}
onClick={() => setRecoveryModalView(true)}
>
{constants.DOWNLOAD_RECOVERY_KEY}
</LinkButton>
</>
<LinkButton <LinkButton
style={{ marginTop: '30px' }} style={{ marginTop: '30px' }}
onClick={() => setRecoveryModalView(true)} onClick={() => {
setData(LS_KEYS.SHOW_BACK_BUTTON, { value: true });
router.push('changePassword');
}}
> >
{constants.DOWNLOAD_RECOVERY_KEY} {constants.CHANGE_PASSWORD}
</LinkButton> </LinkButton>
</> <LinkButton style={{ marginTop: '30px' }} onClick={exportFiles}>
<LinkButton {constants.EXPORT}
style={{ marginTop: '30px' }} </LinkButton>
onClick={() => { <div
setData(LS_KEYS.SHOW_BACK_BUTTON, { value: true }); style={{
router.push('changePassword'); height: '1px',
}} marginTop: '40px',
> background: '#242424',
{constants.CHANGE_PASSWORD} width: '100%',
</LinkButton> }}
<LinkButton style={{ marginTop: '30px' }} onClick={exportFiles}> ></div>
{constants.EXPORT} <LinkButton
</LinkButton> variant="danger"
<div style={{ marginTop: '30px' }}
style={{ onClick={() =>
height: '1px', props.setDialogMessage({
marginTop: '40px', title: `${constants.CONFIRM} ${constants.LOGOUT}`,
background: '#242424', content: constants.LOGOUT_MESSAGE,
width: '100%', staticBackdrop: true,
}} proceed: {
></div> text: constants.LOGOUT,
<LinkButton action: logoutUser,
variant="danger" variant: 'danger',
style={{ marginTop: '30px' }} },
onClick={() => close: { text: constants.CANCEL },
props.setDialogMessage({ })
title: `${constants.CONFIRM} ${constants.LOGOUT}`, }
content: constants.LOGOUT_MESSAGE, >
staticBackdrop: true, logout
proceed: { </LinkButton>
text: constants.LOGOUT, </div>
action: logoutUser,
variant: 'danger',
},
close: { text: constants.CANCEL },
})
}
>
logout
</LinkButton>
<div style={{ marginBottom: '50px' }} />
</Menu> </Menu>
); );
} }

View file

@ -1,72 +0,0 @@
import { useState } from 'react';
import styled from 'styled-components';
const SCROLL_SPEED = 2;
export enum SCROLL_DIRECTION {
LEFT = -1,
RIGHT = +1,
}
interface Props {
collectionRef: React.MutableRefObject<HTMLDivElement>;
scrollDirection: SCROLL_DIRECTION;
}
const Wrapper = styled.div<{ direction: SCROLL_DIRECTION }>`
margin-${(props) =>
props.direction === SCROLL_DIRECTION.LEFT ? 'right' : 'left'}: 10px;
cursor: pointer;
height: 40px;
margin-top: 10px;
${(props) =>
props.direction === SCROLL_DIRECTION.RIGHT &&
`transform:rotate(180deg);`}
&:hover {
color:#fff;
}
`;
const NavigationButton = (props: Props) => {
const [scrollTimeOut, setScrollTimeOut] = useState<NodeJS.Timeout>(null);
if (!props.collectionRef?.current) {
return <div />;
}
let {
clientWidth,
clientHeight,
scrollWidth,
scrollHeight,
} = props.collectionRef.current;
if (scrollHeight <= clientHeight && scrollWidth <= clientWidth) {
return <div />;
}
const scrollStart = () =>
setScrollTimeOut(
setInterval(function () {
props.collectionRef.current.scrollLeft +=
props.scrollDirection * SCROLL_SPEED;
}, 0)
);
const scrollEnd = () => clearTimeout(scrollTimeOut);
return (
<Wrapper
direction={props.scrollDirection}
onMouseDown={scrollStart}
onMouseUp={scrollEnd}
onTouchStart={scrollStart}
onTouchEnd={scrollEnd}
>
<svg
xmlns="http://www.w3.org/2000/svg"
height="40"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
>
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
</svg>
</Wrapper>
);
};
export default NavigationButton;

View file

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import styled, { createGlobalStyle } from 'styled-components'; import styled, { createGlobalStyle } from 'styled-components';
import Navbar from 'components/Navbar'; import Navbar from 'components/Navbar';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
@ -7,9 +7,8 @@ import Container from 'components/Container';
import Head from 'next/head'; import Head from 'next/head';
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
import 'photoswipe/dist/photoswipe.css'; import 'photoswipe/dist/photoswipe.css';
import { Workbox } from "workbox-window";
import { sentryInit } from '../utils/sentry'; import { sentryInit } from '../utils/sentry';
import { useDropzone } from 'react-dropzone';
import EnteSpinner from 'components/EnteSpinner'; import EnteSpinner from 'components/EnteSpinner';
const GlobalStyles = createGlobalStyle` const GlobalStyles = createGlobalStyle`
@ -195,13 +194,29 @@ const GlobalStyles = createGlobalStyle`
} }
.bm-menu { .bm-menu {
background: #131313; background: #131313;
padding: 2.5em 1.5em 0;
font-size: 1.15em; font-size: 1.15em;
color:#d1d1d1 color:#d1d1d1;
display: flex;
} }
.bm-cross { .bm-cross {
background: #d1d1d1; background: #d1d1d1;
} }
.bm-cross-button {
top: 20px !important;
}
.bm-item-list {
display: flex !important;
flex-direction: column;
max-height: 100%;
flex: 1;
}
.bm-item {
padding: 20px;
}
.bm-overlay {
top: 0;
background: rgba(0, 0, 0, 0.8) !important;
}
.bg-upload-progress-bar { .bg-upload-progress-bar {
background-color: #2dc262; background-color: #2dc262;
} }
@ -227,23 +242,23 @@ const GlobalStyles = createGlobalStyle`
width: calc(2.0rem - 4px); width: calc(2.0rem - 4px);
height: calc(2.0rem - 4px); height: calc(2.0rem - 4px);
border-radius: calc(2rem - (2.0rem / 2)); border-radius: calc(2rem - (2.0rem / 2));
left: -38px;
} }
.custom-switch.custom-switch-md .custom-control-input:checked ~ .custom-control-label::after { .custom-switch.custom-switch-md .custom-control-input:checked ~ .custom-control-label::after {
transform: translateX(calc(2.0rem - 0.25rem)); transform: translateX(calc(2.0rem - 0.25rem));
background:#c4c4c4; background:#c4c4c4;
} }
.custom-control-input:checked ~ .custom-control-label::before {
background-color: #29a354;
}
.bold-text{ .bold-text{
color: #ECECEC; color: #ECECEC;
line-height: 24px; line-height: 24px;
font-size: 24px; font-size: 24px;
} }
.subscription-plan-selector {
background: #222;
}
.subscription-plan-selector:hover {
background: #1b1b1b;
}
.dropdown-item:active{ .dropdown-item:active{
color: #16181b; color: #16181b;
text-decoration: none; text-decoration: none;
@ -276,6 +291,14 @@ const FlexContainer = styled.div`
text-align: center; text-align: center;
`; `;
const OfflineContainer = styled.div`
background-color: #111;
padding: 5px 0;
font-size: 14px;
text-align: center;
margin-top: -10px;
`;
export interface BannerMessage { export interface BannerMessage {
message: string; message: string;
variant: string; variant: string;
@ -285,6 +308,22 @@ sentryInit();
export default function App({ Component, pageProps, err }) { export default function App({ Component, pageProps, err }) {
const router = useRouter(); const router = useRouter();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [offline, setOffline] = useState(typeof window !== 'undefined' && !window.navigator.onLine);
useEffect(() => {
if (
!("serviceWorker" in navigator) ||
process.env.NODE_ENV !== "production"
) {
console.warn("Progressive Web App support is disabled");
return;
}
const wb = new Workbox("sw.js", { scope: "/" });
wb.register();
}, []);
const setUserOnline = () => setOffline(false);
const setUserOffline = () => setOffline(true);
useEffect(() => { useEffect(() => {
console.log( console.log(
@ -302,7 +341,17 @@ export default function App({ Component, pageProps, err }) {
router.events.on('routeChangeComplete', () => { router.events.on('routeChangeComplete', () => {
setLoading(false); setLoading(false);
}); });
window.addEventListener('online', setUserOnline);
window.addEventListener('offline', setUserOffline);
return () => {
window.removeEventListener('online', setUserOnline);
window.removeEventListener('offline', setUserOffline);
}
}, []); }, []);
return ( return (
<> <>
<Head> <Head>
@ -325,6 +374,7 @@ export default function App({ Component, pageProps, err }) {
/> />
</FlexContainer> </FlexContainer>
</Navbar> </Navbar>
{offline && <OfflineContainer>{constants.OFFLINE_MSG}</OfflineContainer>}
{loading ? ( {loading ? (
<Container> <Container>
<EnteSpinner> <EnteSpinner>

View file

@ -37,6 +37,7 @@ export default class MyDocument extends Document {
content="ente is a privacy focussed photo storage service that offers end-to-end encryption." content="ente is a privacy focussed photo storage service that offers end-to-end encryption."
/> />
<link rel="icon" href="/icon.svg" type="image/png" /> <link rel="icon" href="/icon.svg" type="image/png" />
<link rel="manifest" href="manifest.json"></link>
</Head> </Head>
<body> <body>
<Main /> <Main />

View file

@ -2,8 +2,8 @@ import CollectionShare from 'components/CollectionShare';
import { SetDialogMessage } from 'components/MessageDialog'; import { SetDialogMessage } from 'components/MessageDialog';
import NavigationButton, { import NavigationButton, {
SCROLL_DIRECTION, SCROLL_DIRECTION,
} from 'components/navigationButton'; } from 'components/NavigationButton';
import React, { useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { OverlayTrigger } from 'react-bootstrap'; import { OverlayTrigger } from 'react-bootstrap';
import { Collection, CollectionType } from 'services/collectionService'; import { Collection, CollectionType } from 'services/collectionService';
import { User } from 'services/userService'; import { User } from 'services/userService';
@ -30,6 +30,7 @@ const Container = styled.div`
height: 50px; height: 50px;
display: flex; display: flex;
max-width: 100%; max-width: 100%;
position: relative;
@media (min-width: 1000px) { @media (min-width: 1000px) {
width: 1000px; width: 1000px;
} }
@ -50,6 +51,7 @@ const Wrapper = styled.div`
white-space: nowrap; white-space: nowrap;
overflow: auto; overflow: auto;
max-width: 100%; max-width: 100%;
scroll-behavior: smooth;
`; `;
const Chip = styled.button<{ active: boolean }>` const Chip = styled.button<{ active: boolean }>`
@ -77,6 +79,20 @@ export default function Collections(props: CollectionProps) {
const collectionRef = useRef<HTMLDivElement>(null); const collectionRef = useRef<HTMLDivElement>(null);
const [collectionShareModalView, setCollectionShareModalView] = const [collectionShareModalView, setCollectionShareModalView] =
useState(false); useState(false);
const [scrollObj, setScrollObj] = useState<{
scrollLeft?: number, scrollWidth?: number, clientWidth?: number
}>({});
const updateScrollObj = () => {
if (collectionRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = collectionRef.current;
setScrollObj({ scrollLeft, scrollWidth, clientWidth });
}
}
useEffect(() => {
updateScrollObj();
}, [collectionRef.current]);
useEffect(() => { useEffect(() => {
if (!collectionRef?.current) { if (!collectionRef?.current) {
@ -89,11 +105,13 @@ export default function Collections(props: CollectionProps) {
setSelectedCollectionID(collection?.id); setSelectedCollectionID(collection?.id);
selectCollection(collection?.id); selectCollection(collection?.id);
}; };
const user: User = getData(LS_KEYS.USER); const user: User = getData(LS_KEYS.USER);
if (!collections || collections.length === 0) { if (!collections || collections.length === 0) {
return <Container />; return null;
} }
const collectionOptions = CollectionOptions({ const collectionOptions = CollectionOptions({
syncWithRemote: props.syncWithRemote, syncWithRemote: props.syncWithRemote,
setCollectionNamerAttributes: props.setCollectionNamerAttributes, setCollectionNamerAttributes: props.setCollectionNamerAttributes,
@ -104,6 +122,11 @@ export default function Collections(props: CollectionProps) {
showCollectionShareModal: setCollectionShareModalView.bind(null, true), showCollectionShareModal: setCollectionShareModalView.bind(null, true),
redirectToAll: selectCollection.bind(null, null), redirectToAll: selectCollection.bind(null, null),
}); });
const scrollCollection = (direction: SCROLL_DIRECTION) => () => {
collectionRef.current.scrollBy(250 * direction, 0);
}
return ( return (
<> <>
<CollectionShare <CollectionShare
@ -116,11 +139,11 @@ export default function Collections(props: CollectionProps) {
syncWithRemote={props.syncWithRemote} syncWithRemote={props.syncWithRemote}
/> />
<Container> <Container>
<NavigationButton {scrollObj.scrollLeft > 0 && <NavigationButton
collectionRef={collectionRef}
scrollDirection={SCROLL_DIRECTION.LEFT} scrollDirection={SCROLL_DIRECTION.LEFT}
/> onClick={scrollCollection(SCROLL_DIRECTION.LEFT)}
<Wrapper ref={collectionRef}> />}
<Wrapper ref={collectionRef} onScroll={updateScrollObj}>
<Chip active={!selected} onClick={clickHandler()}> <Chip active={!selected} onClick={clickHandler()}>
All All
<div <div
@ -159,10 +182,10 @@ export default function Collections(props: CollectionProps) {
</Chip> </Chip>
))} ))}
</Wrapper> </Wrapper>
<NavigationButton {scrollObj.scrollLeft < (scrollObj.scrollWidth - scrollObj.clientWidth) && <NavigationButton
collectionRef={collectionRef}
scrollDirection={SCROLL_DIRECTION.RIGHT} scrollDirection={SCROLL_DIRECTION.RIGHT}
/> onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)}
/>}
</Container> </Container>
</> </>
); );

View file

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Form, Modal } from 'react-bootstrap'; import { Form, Modal, Button } from 'react-bootstrap';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import styled from 'styled-components'; import styled from 'styled-components';
import billingService, { Plan, Subscription } from 'services/billingService'; import billingService, { Plan, Subscription } from 'services/billingService';
@ -21,18 +21,47 @@ import { DeadCenter, SetLoading } from '..';
import LinkButton from './LinkButton'; import LinkButton from './LinkButton';
import { reverseString } from 'utils/common'; import { reverseString } from 'utils/common';
import { SetDialogMessage } from 'components/MessageDialog'; import { SetDialogMessage } from 'components/MessageDialog';
import ArrowEast from 'components/ArrowEast';
export const PlanIcon = styled.div<{ selected: boolean }>` export const PlanIcon = styled.div<{ selected: boolean }>`
padding-top: 20px; border-radius: 20px;
border-radius: 10%;
height: 192px;
width: 250px; width: 250px;
border: 2px solid #868686; border: 2px solid #333;
padding: 30px;
margin: 10px; margin: 10px;
text-align: center; text-align: center;
font-size: 20px; font-size: 20px;
background-color: #ffffff00;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
cursor: ${(props) => (props.selected ? 'not-allowed' : 'pointer')}; cursor: ${(props) => (props.selected ? 'not-allowed' : 'pointer')};
border-color: ${(props) => props.selected && '#56e066'}; border-color: ${(props) => props.selected && '#56e066'};
transition: all 0.3s ease-out;
overflow: hidden;
position: relative;
& > div:first-child::before {
content: ' ';
height: 600px;
width: 50px;
background-color: #444;
left: 0;
top: -50%;
position: absolute;
transform: rotate(45deg) translateX(-200px);
transition: all 0.5s ease-out;
}
&:hover {
transform: scale(1.2);
background-color: #ffffff11;
}
&:hover > div:first-child::before {
transform: rotate(45deg) translateX(300px);
}
`; `;
interface Props { interface Props {
@ -119,10 +148,6 @@ function PlanSelector(props: Props) {
?.map((plan) => ( ?.map((plan) => (
<PlanIcon <PlanIcon
key={plan.stripeID} key={plan.stripeID}
onClick={async () =>
!isUserSubscribedPlan(plan, subscription) &&
(await onPlanSelect(plan))
}
className="subscription-plan-selector" className="subscription-plan-selector"
selected={isUserSubscribedPlan(plan, subscription)} selected={isUserSubscribedPlan(plan, subscription)}
> >
@ -132,6 +157,7 @@ function PlanSelector(props: Props) {
color: '#ECECEC', color: '#ECECEC',
fontWeight: 900, fontWeight: 900,
fontSize: '72px', fontSize: '72px',
lineHeight: '72px'
}} }}
> >
{convertBytesToGBs(plan.storage, 0)} {convertBytesToGBs(plan.storage, 0)}
@ -149,8 +175,18 @@ function PlanSelector(props: Props) {
</div> </div>
<div <div
className={`bold-text`} className={`bold-text`}
style={{ color: '#aaa' }} style={{ color: '#aaa', lineHeight: '36px', fontSize: '20px' }}
>{`${plan.price} / ${plan.period}`}</div> >{`${plan.price} / ${plan.period}`}</div>
<Button
variant="outline-success"
block
style={{ marginTop: '30px'}}
disabled={isUserSubscribedPlan(plan, subscription)}
onClick={async () => (await onPlanSelect(plan))}
>
{constants.CHOOSE_PLAN_BTN}
<ArrowEast style={{ marginLeft: '10px' }} />
</Button>
</PlanIcon> </PlanIcon>
)); ));
return ( return (
@ -183,13 +219,13 @@ function PlanSelector(props: Props) {
className={`bold-text`} className={`bold-text`}
style={{ fontSize: '20px' }} style={{ fontSize: '20px' }}
> >
{constants.YEARLY} {constants.MONTHLY}
</span> </span>
<Form.Switch <Form.Switch
checked={planPeriod == PLAN_PERIOD.MONTH} checked={planPeriod === PLAN_PERIOD.YEAR}
id={`plan-period-toggler`} id={`plan-period-toggler`}
style={{ marginLeft: '15px', marginTop: '-4px' }} style={{ margin: '-4px 0 20px 15px' }}
className={`custom-switch-md`} className={`custom-switch-md`}
onChange={togglePeriod} onChange={togglePeriod}
/> />
@ -197,7 +233,7 @@ function PlanSelector(props: Props) {
className={`bold-text`} className={`bold-text`}
style={{ fontSize: '20px' }} style={{ fontSize: '20px' }}
> >
{constants.MONTHLY} {constants.YEARLY}
</span> </span>
</div> </div>
</DeadCenter> </DeadCenter>
@ -290,7 +326,7 @@ function PlanSelector(props: Props) {
<LinkButton <LinkButton
variant="primary" variant="primary"
onClick={props.closeModal} onClick={props.closeModal}
style={{ color: 'rgb(121, 121, 121)' }} style={{ color: 'rgb(121, 121, 121)', marginTop: '20px' }}
> >
{isOnFreePlan(subscription) {isOnFreePlan(subscription)
? constants.SKIP ? constants.SKIP

13
src/serviceWorker.js Normal file
View file

@ -0,0 +1,13 @@
import {precacheAndRoute} from 'workbox-precaching';
import {setDefaultHandler} from 'workbox-routing';
import {StaleWhileRevalidate} from 'workbox-strategies';
import { pageCache, offlineFallback } from 'workbox-recipes';
pageCache();
precacheAndRoute(self.__WB_MANIFEST);
// Use a stale-while-revalidate strategy for all other requests.
setDefaultHandler(new StaleWhileRevalidate());
offlineFallback();

View file

@ -197,6 +197,9 @@ const englishConstants = {
MANAGEMENT_PORTAL: 'manage payment method', MANAGEMENT_PORTAL: 'manage payment method',
CHOOSE_PLAN: 'choose your subscription plan', CHOOSE_PLAN: 'choose your subscription plan',
MANAGE_PLAN: 'manage your subscription', MANAGE_PLAN: 'manage your subscription',
CHOOSE_PLAN_BTN: 'choose plan',
OFFLINE_MSG: 'you are offline, cached memories are being shown',
FREE_SUBSCRIPTION_INFO: (expiryTime) => ( FREE_SUBSCRIPTION_INFO: (expiryTime) => (
<> <>

1311
yarn.lock

File diff suppressed because it is too large Load diff