commit
4e09320f8e
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
|
|
|
@ -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
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
27
public/manifest.json
Normal 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
9
public/offline.html
Normal 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>
|
22
src/components/ArrowEast.tsx
Normal file
22
src/components/ArrowEast.tsx
Normal 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',
|
||||||
|
};
|
22
src/components/CloudUpload.tsx
Normal file
22
src/components/CloudUpload.tsx
Normal 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',
|
||||||
|
};
|
21
src/components/NavigateNext.tsx
Normal file
21
src/components/NavigateNext.tsx
Normal 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',
|
||||||
|
};
|
62
src/components/NavigationButton.tsx
Normal file
62
src/components/NavigationButton.tsx
Normal 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;
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
13
src/serviceWorker.js
Normal 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();
|
|
@ -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) => (
|
||||||
<>
|
<>
|
||||||
|
|
Loading…
Reference in a new issue