commit
4e09320f8e
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -38,3 +38,9 @@ out_publish
|
|||
|
||||
.env
|
||||
/.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')({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
const withWorkbox = require("next-with-workbox");
|
||||
|
||||
const {
|
||||
NEXT_PUBLIC_SENTRY_DSN: SENTRY_DSN,
|
||||
|
@ -17,7 +18,7 @@ const {
|
|||
process.env.SENTRY_DSN = SENTRY_DSN;
|
||||
const basePath = '';
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
module.exports = withWorkbox(withBundleAnalyzer({
|
||||
target: 'serverless',
|
||||
productionBrowserSourceMaps: true,
|
||||
env: {
|
||||
|
@ -26,6 +27,9 @@ module.exports = withBundleAnalyzer({
|
|||
// outside of Vercel
|
||||
NEXT_PUBLIC_COMMIT_SHA: COMMIT_SHA,
|
||||
},
|
||||
workbox: {
|
||||
swSrc: "src/serviceWorker.js",
|
||||
},
|
||||
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
|
||||
if (!isServer) {
|
||||
config.plugins.push(
|
||||
|
@ -64,4 +68,4 @@ module.exports = withBundleAnalyzer({
|
|||
}
|
||||
return config;
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"libsodium-wrappers": "^0.7.8",
|
||||
"localforage": "^1.9.0",
|
||||
"next": "9.5.3",
|
||||
"next-with-workbox": "^2.0.1",
|
||||
"node-forge": "^0.10.0",
|
||||
"photoswipe": "file:./thirdparty/photoswipe",
|
||||
"react": "16.13.1",
|
||||
|
@ -39,6 +40,11 @@
|
|||
"react-window-infinite-loader": "^1.0.5",
|
||||
"scrypt-js": "^3.0.1",
|
||||
"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",
|
||||
"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 { VariableSizeList as List } from 'react-window';
|
||||
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
|
||||
import CloudUpload from './CloudUpload';
|
||||
|
||||
const DATE_CONTAINER_HEIGHT = 45;
|
||||
const IMAGE_CONTAINER_HEIGHT = 200;
|
||||
|
@ -70,6 +71,19 @@ const DateContainer = styled.div`
|
|||
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 {
|
||||
TIME = 'TIME',
|
||||
TILE = 'TILE',
|
||||
|
@ -261,13 +275,8 @@ const PhotoFrame = ({
|
|||
return (
|
||||
<>
|
||||
{!isFirstLoad && files.length == 0 ? (
|
||||
<div
|
||||
style={{
|
||||
height: '60%',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
}}
|
||||
>
|
||||
<EmptyScreen>
|
||||
<CloudUpload width={150} height={150} />
|
||||
<Button
|
||||
variant="outline-success"
|
||||
onClick={openFileUploader}
|
||||
|
@ -280,7 +289,7 @@ const PhotoFrame = ({
|
|||
>
|
||||
{constants.UPLOAD_FIRST_PHOTO}
|
||||
</Button>
|
||||
</div>
|
||||
</EmptyScreen>
|
||||
) : filteredData.length ? (
|
||||
<Container>
|
||||
<AutoSizer>
|
||||
|
|
|
@ -65,6 +65,15 @@ function PhotoSwipe(props: Iprops) {
|
|||
maxSpreadZoom: 5,
|
||||
index: currentIndex,
|
||||
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(
|
||||
pswpElement,
|
||||
|
|
|
@ -104,7 +104,7 @@ export default function Sidebar(props: Props) {
|
|||
>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '28px',
|
||||
marginBottom: '8px',
|
||||
outline: 'none',
|
||||
color: 'rgb(45, 194, 98)',
|
||||
fontSize: '16px',
|
||||
|
@ -112,147 +112,141 @@ export default function Sidebar(props: Props) {
|
|||
>
|
||||
{user?.email}
|
||||
</div>
|
||||
<div style={{ outline: 'none' }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<h5 style={{ margin: '4px 0 12px 2px' }}>
|
||||
{constants.SUBSCRIPTION_PLAN}
|
||||
</h5>
|
||||
<div style={{ marginLeft: '10px' }}>
|
||||
{
|
||||
<Button
|
||||
variant={
|
||||
isSubscribed(subscription)
|
||||
? 'outline-secondary'
|
||||
: 'outline-success'
|
||||
}
|
||||
size="sm"
|
||||
onClick={onManageClick}
|
||||
>
|
||||
{isSubscribed(subscription)
|
||||
? constants.MANAGE
|
||||
: constants.SUBSCRIBE}
|
||||
</Button>
|
||||
}
|
||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
||||
<div style={{ outline: 'none' }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<h5 style={{ margin: '4px 0 12px 2px' }}>
|
||||
{constants.SUBSCRIPTION_PLAN}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ color: '#959595' }}>
|
||||
{isSubscriptionActive(subscription) ? (
|
||||
isOnFreePlan(subscription) ? (
|
||||
constants.FREE_SUBSCRIPTION_INFO(
|
||||
subscription?.expiryTime
|
||||
)
|
||||
) : isSubscriptionCancelled(subscription) ? (
|
||||
constants.RENEWAL_CANCELLED_SUBSCRIPTION_INFO(
|
||||
subscription?.expiryTime
|
||||
<div style={{ color: '#959595' }}>
|
||||
{isSubscriptionActive(subscription) ? (
|
||||
isOnFreePlan(subscription) ? (
|
||||
constants.FREE_SUBSCRIPTION_INFO(
|
||||
subscription?.expiryTime
|
||||
)
|
||||
) : isSubscriptionCancelled(subscription) ? (
|
||||
constants.RENEWAL_CANCELLED_SUBSCRIPTION_INFO(
|
||||
subscription?.expiryTime
|
||||
)
|
||||
) : (
|
||||
constants.RENEWAL_ACTIVE_SUBSCRIPTION_INFO(
|
||||
subscription?.expiryTime
|
||||
)
|
||||
)
|
||||
) : (
|
||||
constants.RENEWAL_ACTIVE_SUBSCRIPTION_INFO(
|
||||
subscription?.expiryTime
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<p>{constants.SUBSCRIPTION_EXPIRED}</p>
|
||||
)}
|
||||
<p>{constants.SUBSCRIPTION_EXPIRED}</p>
|
||||
)}
|
||||
<Button
|
||||
variant="outline-success"
|
||||
block
|
||||
size="sm"
|
||||
onClick={onManageClick}
|
||||
>
|
||||
{isSubscribed(subscription)
|
||||
? constants.MANAGE
|
||||
: constants.SUBSCRIBE}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ outline: 'none', marginTop: '30px' }}></div>
|
||||
<div>
|
||||
<h5 style={{ marginBottom: '12px' }}>
|
||||
{constants.USAGE_DETAILS}
|
||||
</h5>
|
||||
<div style={{ color: '#959595' }}>
|
||||
{usage ? (
|
||||
constants.USAGE_INFO(
|
||||
usage,
|
||||
Math.ceil(
|
||||
Number(convertBytesToGBs(subscription?.storage))
|
||||
<div style={{ outline: 'none', marginTop: '30px' }}></div>
|
||||
<div>
|
||||
<h5 style={{ marginBottom: '12px' }}>
|
||||
{constants.USAGE_DETAILS}
|
||||
</h5>
|
||||
<div style={{ color: '#959595' }}>
|
||||
{usage ? (
|
||||
constants.USAGE_INFO(
|
||||
usage,
|
||||
Math.ceil(
|
||||
Number(convertBytesToGBs(subscription?.storage))
|
||||
)
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<EnteSpinner
|
||||
style={{
|
||||
borderWidth: '2px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<EnteSpinner
|
||||
style={{
|
||||
borderWidth: '2px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: '1px',
|
||||
marginTop: '40px',
|
||||
background: '#242424',
|
||||
width: '100%',
|
||||
}}
|
||||
></div>
|
||||
<LinkButton style={{ marginTop: '30px' }} onClick={openFeedbackURL}>
|
||||
{constants.REQUEST_FEATURE}
|
||||
</LinkButton>
|
||||
<LinkButton style={{ marginTop: '30px' }} onClick={openSupportMail}>
|
||||
{constants.SUPPORT}
|
||||
</LinkButton>
|
||||
<>
|
||||
<RecoveryKeyModal
|
||||
show={recoverModalView}
|
||||
onHide={() => setRecoveryModalView(false)}
|
||||
somethingWentWrong={() =>
|
||||
props.setDialogMessage({
|
||||
title: constants.RECOVER_KEY_GENERATION_FAILED,
|
||||
close: { variant: 'danger' },
|
||||
})
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
height: '1px',
|
||||
marginTop: '40px',
|
||||
background: '#242424',
|
||||
width: '100%',
|
||||
}}
|
||||
></div>
|
||||
<LinkButton style={{ marginTop: '30px' }} onClick={openFeedbackURL}>
|
||||
{constants.REQUEST_FEATURE}
|
||||
</LinkButton>
|
||||
<LinkButton style={{ marginTop: '30px' }} onClick={openSupportMail}>
|
||||
{constants.SUPPORT}
|
||||
</LinkButton>
|
||||
<>
|
||||
<RecoveryKeyModal
|
||||
show={recoverModalView}
|
||||
onHide={() => setRecoveryModalView(false)}
|
||||
somethingWentWrong={() =>
|
||||
props.setDialogMessage({
|
||||
title: constants.RECOVER_KEY_GENERATION_FAILED,
|
||||
close: { variant: 'danger' },
|
||||
})
|
||||
}
|
||||
/>
|
||||
<LinkButton
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={() => setRecoveryModalView(true)}
|
||||
>
|
||||
{constants.DOWNLOAD_RECOVERY_KEY}
|
||||
</LinkButton>
|
||||
</>
|
||||
<LinkButton
|
||||
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
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={() => {
|
||||
setData(LS_KEYS.SHOW_BACK_BUTTON, { value: true });
|
||||
router.push('changePassword');
|
||||
}}
|
||||
>
|
||||
{constants.CHANGE_PASSWORD}
|
||||
</LinkButton>
|
||||
<LinkButton style={{ marginTop: '30px' }} onClick={exportFiles}>
|
||||
{constants.EXPORT}
|
||||
</LinkButton>
|
||||
<div
|
||||
style={{
|
||||
height: '1px',
|
||||
marginTop: '40px',
|
||||
background: '#242424',
|
||||
width: '100%',
|
||||
}}
|
||||
></div>
|
||||
<LinkButton
|
||||
variant="danger"
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={() =>
|
||||
props.setDialogMessage({
|
||||
title: `${constants.CONFIRM} ${constants.LOGOUT}`,
|
||||
content: constants.LOGOUT_MESSAGE,
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
text: constants.LOGOUT,
|
||||
action: logoutUser,
|
||||
variant: 'danger',
|
||||
},
|
||||
close: { text: constants.CANCEL },
|
||||
})
|
||||
}
|
||||
>
|
||||
logout
|
||||
</LinkButton>
|
||||
<div style={{ marginBottom: '50px' }} />
|
||||
<LinkButton style={{ marginTop: '30px' }} onClick={exportFiles}>
|
||||
{constants.EXPORT}
|
||||
</LinkButton>
|
||||
<div
|
||||
style={{
|
||||
height: '1px',
|
||||
marginTop: '40px',
|
||||
background: '#242424',
|
||||
width: '100%',
|
||||
}}
|
||||
></div>
|
||||
<LinkButton
|
||||
variant="danger"
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={() =>
|
||||
props.setDialogMessage({
|
||||
title: `${constants.CONFIRM} ${constants.LOGOUT}`,
|
||||
content: constants.LOGOUT_MESSAGE,
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
text: constants.LOGOUT,
|
||||
action: logoutUser,
|
||||
variant: 'danger',
|
||||
},
|
||||
close: { text: constants.CANCEL },
|
||||
})
|
||||
}
|
||||
>
|
||||
logout
|
||||
</LinkButton>
|
||||
</div>
|
||||
</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 Navbar from 'components/Navbar';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
@ -7,9 +7,8 @@ import Container from 'components/Container';
|
|||
import Head from 'next/head';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'photoswipe/dist/photoswipe.css';
|
||||
|
||||
import { Workbox } from "workbox-window";
|
||||
import { sentryInit } from '../utils/sentry';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
|
||||
const GlobalStyles = createGlobalStyle`
|
||||
|
@ -195,13 +194,29 @@ const GlobalStyles = createGlobalStyle`
|
|||
}
|
||||
.bm-menu {
|
||||
background: #131313;
|
||||
padding: 2.5em 1.5em 0;
|
||||
font-size: 1.15em;
|
||||
color:#d1d1d1
|
||||
color:#d1d1d1;
|
||||
display: flex;
|
||||
}
|
||||
.bm-cross {
|
||||
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 {
|
||||
background-color: #2dc262;
|
||||
}
|
||||
|
@ -227,23 +242,23 @@ const GlobalStyles = createGlobalStyle`
|
|||
width: calc(2.0rem - 4px);
|
||||
height: calc(2.0rem - 4px);
|
||||
border-radius: calc(2rem - (2.0rem / 2));
|
||||
left: -38px;
|
||||
}
|
||||
|
||||
.custom-switch.custom-switch-md .custom-control-input:checked ~ .custom-control-label::after {
|
||||
transform: translateX(calc(2.0rem - 0.25rem));
|
||||
background:#c4c4c4;
|
||||
}
|
||||
|
||||
.custom-control-input:checked ~ .custom-control-label::before {
|
||||
background-color: #29a354;
|
||||
}
|
||||
|
||||
.bold-text{
|
||||
color: #ECECEC;
|
||||
line-height: 24px;
|
||||
font-size: 24px;
|
||||
}
|
||||
.subscription-plan-selector {
|
||||
background: #222;
|
||||
}
|
||||
.subscription-plan-selector:hover {
|
||||
background: #1b1b1b;
|
||||
}
|
||||
.dropdown-item:active{
|
||||
color: #16181b;
|
||||
text-decoration: none;
|
||||
|
@ -276,6 +291,14 @@ const FlexContainer = styled.div`
|
|||
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 {
|
||||
message: string;
|
||||
variant: string;
|
||||
|
@ -285,6 +308,22 @@ sentryInit();
|
|||
export default function App({ Component, pageProps, err }) {
|
||||
const router = useRouter();
|
||||
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(() => {
|
||||
console.log(
|
||||
|
@ -302,7 +341,17 @@ export default function App({ Component, pageProps, err }) {
|
|||
router.events.on('routeChangeComplete', () => {
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
window.addEventListener('online', setUserOnline);
|
||||
window.addEventListener('offline', setUserOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', setUserOnline);
|
||||
window.removeEventListener('offline', setUserOffline);
|
||||
}
|
||||
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
|
@ -325,6 +374,7 @@ export default function App({ Component, pageProps, err }) {
|
|||
/>
|
||||
</FlexContainer>
|
||||
</Navbar>
|
||||
{offline && <OfflineContainer>{constants.OFFLINE_MSG}</OfflineContainer>}
|
||||
{loading ? (
|
||||
<Container>
|
||||
<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."
|
||||
/>
|
||||
<link rel="icon" href="/icon.svg" type="image/png" />
|
||||
<link rel="manifest" href="manifest.json"></link>
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
|
|
|
@ -2,8 +2,8 @@ import CollectionShare from 'components/CollectionShare';
|
|||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
import NavigationButton, {
|
||||
SCROLL_DIRECTION,
|
||||
} from 'components/navigationButton';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
} from 'components/NavigationButton';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { OverlayTrigger } from 'react-bootstrap';
|
||||
import { Collection, CollectionType } from 'services/collectionService';
|
||||
import { User } from 'services/userService';
|
||||
|
@ -30,6 +30,7 @@ const Container = styled.div`
|
|||
height: 50px;
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
@media (min-width: 1000px) {
|
||||
width: 1000px;
|
||||
}
|
||||
|
@ -50,6 +51,7 @@ const Wrapper = styled.div`
|
|||
white-space: nowrap;
|
||||
overflow: auto;
|
||||
max-width: 100%;
|
||||
scroll-behavior: smooth;
|
||||
`;
|
||||
|
||||
const Chip = styled.button<{ active: boolean }>`
|
||||
|
@ -77,6 +79,20 @@ export default function Collections(props: CollectionProps) {
|
|||
const collectionRef = useRef<HTMLDivElement>(null);
|
||||
const [collectionShareModalView, setCollectionShareModalView] =
|
||||
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(() => {
|
||||
if (!collectionRef?.current) {
|
||||
|
@ -89,11 +105,13 @@ export default function Collections(props: CollectionProps) {
|
|||
setSelectedCollectionID(collection?.id);
|
||||
selectCollection(collection?.id);
|
||||
};
|
||||
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
|
||||
if (!collections || collections.length === 0) {
|
||||
return <Container />;
|
||||
return null;
|
||||
}
|
||||
|
||||
const collectionOptions = CollectionOptions({
|
||||
syncWithRemote: props.syncWithRemote,
|
||||
setCollectionNamerAttributes: props.setCollectionNamerAttributes,
|
||||
|
@ -104,6 +122,11 @@ export default function Collections(props: CollectionProps) {
|
|||
showCollectionShareModal: setCollectionShareModalView.bind(null, true),
|
||||
redirectToAll: selectCollection.bind(null, null),
|
||||
});
|
||||
|
||||
const scrollCollection = (direction: SCROLL_DIRECTION) => () => {
|
||||
collectionRef.current.scrollBy(250 * direction, 0);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CollectionShare
|
||||
|
@ -116,11 +139,11 @@ export default function Collections(props: CollectionProps) {
|
|||
syncWithRemote={props.syncWithRemote}
|
||||
/>
|
||||
<Container>
|
||||
<NavigationButton
|
||||
collectionRef={collectionRef}
|
||||
{scrollObj.scrollLeft > 0 && <NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.LEFT}
|
||||
/>
|
||||
<Wrapper ref={collectionRef}>
|
||||
onClick={scrollCollection(SCROLL_DIRECTION.LEFT)}
|
||||
/>}
|
||||
<Wrapper ref={collectionRef} onScroll={updateScrollObj}>
|
||||
<Chip active={!selected} onClick={clickHandler()}>
|
||||
All
|
||||
<div
|
||||
|
@ -159,10 +182,10 @@ export default function Collections(props: CollectionProps) {
|
|||
</Chip>
|
||||
))}
|
||||
</Wrapper>
|
||||
<NavigationButton
|
||||
collectionRef={collectionRef}
|
||||
{scrollObj.scrollLeft < (scrollObj.scrollWidth - scrollObj.clientWidth) && <NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.RIGHT}
|
||||
/>
|
||||
onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)}
|
||||
/>}
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 styled from 'styled-components';
|
||||
import billingService, { Plan, Subscription } from 'services/billingService';
|
||||
|
@ -21,18 +21,47 @@ import { DeadCenter, SetLoading } from '..';
|
|||
import LinkButton from './LinkButton';
|
||||
import { reverseString } from 'utils/common';
|
||||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
import ArrowEast from 'components/ArrowEast';
|
||||
|
||||
export const PlanIcon = styled.div<{ selected: boolean }>`
|
||||
padding-top: 20px;
|
||||
border-radius: 10%;
|
||||
height: 192px;
|
||||
border-radius: 20px;
|
||||
width: 250px;
|
||||
border: 2px solid #868686;
|
||||
border: 2px solid #333;
|
||||
padding: 30px;
|
||||
margin: 10px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
background-color: #ffffff00;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
cursor: ${(props) => (props.selected ? 'not-allowed' : 'pointer')};
|
||||
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 {
|
||||
|
@ -119,10 +148,6 @@ function PlanSelector(props: Props) {
|
|||
?.map((plan) => (
|
||||
<PlanIcon
|
||||
key={plan.stripeID}
|
||||
onClick={async () =>
|
||||
!isUserSubscribedPlan(plan, subscription) &&
|
||||
(await onPlanSelect(plan))
|
||||
}
|
||||
className="subscription-plan-selector"
|
||||
selected={isUserSubscribedPlan(plan, subscription)}
|
||||
>
|
||||
|
@ -132,6 +157,7 @@ function PlanSelector(props: Props) {
|
|||
color: '#ECECEC',
|
||||
fontWeight: 900,
|
||||
fontSize: '72px',
|
||||
lineHeight: '72px'
|
||||
}}
|
||||
>
|
||||
{convertBytesToGBs(plan.storage, 0)}
|
||||
|
@ -149,8 +175,18 @@ function PlanSelector(props: Props) {
|
|||
</div>
|
||||
<div
|
||||
className={`bold-text`}
|
||||
style={{ color: '#aaa' }}
|
||||
style={{ color: '#aaa', lineHeight: '36px', fontSize: '20px' }}
|
||||
>{`${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>
|
||||
));
|
||||
return (
|
||||
|
@ -183,13 +219,13 @@ function PlanSelector(props: Props) {
|
|||
className={`bold-text`}
|
||||
style={{ fontSize: '20px' }}
|
||||
>
|
||||
{constants.YEARLY}
|
||||
{constants.MONTHLY}
|
||||
</span>
|
||||
|
||||
<Form.Switch
|
||||
checked={planPeriod == PLAN_PERIOD.MONTH}
|
||||
checked={planPeriod === PLAN_PERIOD.YEAR}
|
||||
id={`plan-period-toggler`}
|
||||
style={{ marginLeft: '15px', marginTop: '-4px' }}
|
||||
style={{ margin: '-4px 0 20px 15px' }}
|
||||
className={`custom-switch-md`}
|
||||
onChange={togglePeriod}
|
||||
/>
|
||||
|
@ -197,7 +233,7 @@ function PlanSelector(props: Props) {
|
|||
className={`bold-text`}
|
||||
style={{ fontSize: '20px' }}
|
||||
>
|
||||
{constants.MONTHLY}
|
||||
{constants.YEARLY}
|
||||
</span>
|
||||
</div>
|
||||
</DeadCenter>
|
||||
|
@ -290,7 +326,7 @@ function PlanSelector(props: Props) {
|
|||
<LinkButton
|
||||
variant="primary"
|
||||
onClick={props.closeModal}
|
||||
style={{ color: 'rgb(121, 121, 121)' }}
|
||||
style={{ color: 'rgb(121, 121, 121)', marginTop: '20px' }}
|
||||
>
|
||||
{isOnFreePlan(subscription)
|
||||
? 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',
|
||||
CHOOSE_PLAN: 'choose your subscription plan',
|
||||
MANAGE_PLAN: 'manage your subscription',
|
||||
CHOOSE_PLAN_BTN: 'choose plan',
|
||||
|
||||
OFFLINE_MSG: 'you are offline, cached memories are being shown',
|
||||
|
||||
FREE_SUBSCRIPTION_INFO: (expiryTime) => (
|
||||
<>
|
||||
|
|
Loading…
Reference in a new issue