This commit is contained in:
2020-08-19 22:55:44 +02:00
parent 312c8754bc
commit b84aa3db45
18 changed files with 1184 additions and 146 deletions

View File

@@ -1,19 +1,20 @@
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { hot } from 'react-hot-loader/root';
import { Layout } from 'antd';
import { GithubProvider } from './contexts/Github';
import { EncryptionProvider } from './contexts/Encryption';
import Encrypt from './screens/Encrypt';
import theme from './theme';
import AppRouter from './Router';
const App: React.FC = () => (
<ThemeProvider theme={theme}>
<GithubProvider username="morten-olsen">
<div>Test</div>
<EncryptionProvider>
<Encrypt />
</EncryptionProvider>
</GithubProvider>
</ThemeProvider>
<GithubProvider username="morten-olsen">
<EncryptionProvider>
<Layout>
<Layout.Content style={{ padding: '25px' }}>
<AppRouter/>
</Layout.Content>
</Layout>
</EncryptionProvider>
</GithubProvider>
);
export default App;
export default hot(App);

24
src/Router.tsx Normal file
View File

@@ -0,0 +1,24 @@
import React from 'react';
import {
HashRouter as Router,
Switch,
Route,
} from 'react-router-dom';
import Encrypt from './screens/Encrypt';
import Debug from './screens/Debug';
const AppRouter: React.FC = () => (
<Router>
<Switch>
<Route path="/debug">
<Debug />
</Route>
<Route path="/">
<Encrypt />
</Route>
</Switch>
</Router>
);
export default AppRouter;

View File

@@ -1,49 +1,49 @@
import React, { useState } from 'react';
import styled from 'styled-components';
import { Theme } from '../theme';
import React, { Fragment, useState } from 'react';
import { Menu, Dropdown, Form } from 'antd';
import { DownOutlined, FileOutlined, FileTextOutlined } from '@ant-design/icons';
import AddText from './AddText';
import AddFile from './AddFile';
const Wrapper = styled.div`
`;
const Top = styled.div`
display: flex;
`;
const Button = styled.button<{
active: boolean;
theme: Theme;
}>`
background: ${({ active }) => active ? '#2c3e50' : 'transparent'};
padding: ${({ theme }) => theme.margin.medium}px;
border: none;
font: inherit;
color: inherit;
`;
const Panel = styled.div<{
theme: Theme;
}>`
background: #2c3e50;
color: #fff;
padding: ${({ theme }) => theme.margin.medium}px;
`;
const layout = {
labelCol: { span: 2 },
};
const Add: React.FC = () => {
const [type, setType] = useState<'file' | 'text'>('text');
const menu = (
<Menu>
<Menu.Item
onClick={() => setType('file')}
active={type === 'file'}
icon={<FileOutlined />}
>
File
</Menu.Item>
<Menu.Item
onClick={() => setType('text')}
active={type === 'text'}
icon={<FileTextOutlined />}
>
Text
</Menu.Item>
</Menu>
);
return (
<Wrapper>
<Top>
<Button active={type==='text'} onClick={() => setType('text')}>Text</Button>
<Button active={type==='file'} onClick={() => setType('file')}>File</Button>
</Top>
<Panel>
{type === 'file' && <AddFile />}
{type === 'text' && <AddText />}
</Panel>
</Wrapper>
<>
<Form {...layout}>
<Form.Item
label="I want to encrypt a"
>
<Dropdown overlay={menu}>
<a>{type} <DownOutlined /></a>
</Dropdown>
</Form.Item>
</Form>
{type === 'text' && <AddText />}
{type === 'file' && <AddFile />}
</>
);
};

View File

@@ -1,10 +1,15 @@
import React, { useContext, useCallback } from 'react';
import styled from 'styled-components';
import { Layout } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import { useDropzone } from 'react-dropzone';
import EncryptionContext from '../contexts/Encryption';
import { Upload } from 'react-feather';
const DropWrapper = styled.div`
const Icon = styled(UploadOutlined)`
font-size: 100px;
`;
const DropWrapper = styled(Layout)`
display: flex;
justify-content: center;
align-items: center;
@@ -19,13 +24,11 @@ const AddFile: React.FC = () => {
}, [])
const {getRootProps, getInputProps} = useDropzone({ onDrop });
return (
<div>
<DropWrapper {...getRootProps()}>
<input {...getInputProps()} />
<Upload size={200} />
<Icon />
<p>Drag 'n' drop some files here, or click to select files</p>
</DropWrapper>
</div>
);
};

View File

@@ -1,39 +1,57 @@
import React, { useState, useCallback, useContext } from 'react';
import styled from 'styled-components';
import { Input, Form, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import EncryptionContext from '../contexts/Encryption';
import { Theme } from '../theme';
const Wrapper = styled.div`
display: flex;
flex-direction: column;
flex: 1;
`;
const layout = {
labelCol: { span: 2 },
};
const Text = styled.textarea<{
theme: Theme;
}>`
border: none;
height: 200px;
background: transparent;
color: inherit;
padding: ${({ theme }) => theme.margin.medium}px;
font: inherit;
`;
const tailLayout = {
wrapperCol: { offset: 2 },
};
const AddText : React.FC = () => {
const { addText } = useContext(EncryptionContext);
const [name, setName] = useState('');
const [text, setText] = useState('');
const add = useCallback(() => {
addText(text);
addText(text, name || 'untitled');
setText('');
setName('');
}, [text, addText]);
return (
<Wrapper>
<Text placeholder="Enter you message..." value={text} onChange={evt => setText(evt.target.value)} />
<button onClick={add}>Save</button>
</Wrapper>
<Form {...layout}>
<Form.Item
label="Name"
>
<Input
value={name}
onChange={evt => setName(evt.target.value)}
/>
</Form.Item>
<Form.Item
label="Message"
>
<Input.TextArea
value={text}
rows={10}
onChange={evt => setText(evt.target.value)}
/>
</Form.Item>
<Form.Item {...tailLayout}>
<Button
onClick={add}
type="primary"
icon={<PlusOutlined />}
disabled={!text}
>
Add
</Button>
</Form.Item>
</Form>
);
};

View File

@@ -1,12 +1,20 @@
import React from 'react';
import styled from 'styled-components';
import { File } from '../contexts/Encryption';
import {
List,
Button,
} from 'antd';
import {
DeleteOutlined,
SyncOutlined,
IssuesCloseOutlined,
LockOutlined,
} from '@ant-design/icons';
import { FileType } from '../contexts/Encryption';
import { CheckCircle, XCircle, Download, Trash, Loader } from 'react-feather';
import Row, { Cell } from './Row';
interface Props {
remove: () => void;
file: File;
file: FileType;
}
const downloadLink = (name: string, url: string) => {
@@ -18,42 +26,51 @@ const downloadLink = (name: string, url: string) => {
document.body.removeChild(downloadLink);
};
const Button = styled.button`
background: none;
border: none;
color: green;
`;
const icons: {[name: string]: typeof CheckCircle} = {
encrypting: Loader,
failed: XCircle,
encrypted: CheckCircle,
const icons: {[name: string]: any} = {
encrypting: <SyncOutlined spin />,
failed: <IssuesCloseOutlined />,
encrypted: <LockOutlined />,
};
const IconText = ({ icon, text, ...props }) => (
<Button
{...props}
icon={React.createElement(icon)}
>
{text}
</Button>
);
const FileView: React.FC<Props> = ({
file,
remove,
}) => {
const Icon = icons[file.status];
const icon = icons[file.status];
return (
<Row
left={(
<Cell><Icon /></Cell>
)}
title={file.name}
body={`encrypted for ${file.reciever}`}
right={!!file.link && (
<>
<Cell>
<Button onClick={() => downloadLink(file.name, file.link)}><Download /></Button>
</Cell>
<Cell>
<Button onClick={remove}><Trash /></Button>
</Cell>
</>
)}
/>
<List.Item
actions={file.link ? [(
<IconText
icon={DeleteOutlined}
danger
text="Delete"
onClick={remove}
/>
), (
<IconText
icon={DeleteOutlined}
type="primary"
text="Download"
onClick={() => downloadLink(file.name, file.link!)}
/>
)]: []}
>
<List.Item.Meta
avatar={icon}
title={file.name}
description={`Encrypted for ${file.reciever}`}
/>
</List.Item>
);
};

View File

@@ -1,12 +1,17 @@
import React, { useContext } from 'react';
import { List, Empty } from 'antd';
import EncryptionContext from '../contexts/Encryption';
import File from './File';
const Encrypt: React.FC = () => {
const { files, deleteFile } = useContext(EncryptionContext);
if (Object.keys(files).length === 0) {
return <Empty />
}
return (
<div>
<List>
{Object.entries(files).map(([id, file]) => (
<File
key={id}
@@ -14,7 +19,7 @@ const Encrypt: React.FC = () => {
remove={() => deleteFile(id)}
/>
))}
</div>
</List>
);
};

View File

@@ -0,0 +1,71 @@
import React, { useContext, useState } from 'react';
import {
Card,
Avatar,
Button,
Modal,
} from 'antd';
import { KeyOutlined, GithubOutlined } from '@ant-design/icons'
import GithubContext from '../contexts/Github';
const IconText = ({ icon, text, ...props }) => (
<Button
{...props}
icon={React.createElement(icon)}
>
{text}
</Button>
);
const Profile: React.FC = () => {
const { user, keys } = useContext(GithubContext);
const [showKeys, setShowKeys] = useState(false);
if (!user) {
return null;
}
return (
<>
<Modal
visible={showKeys}
onOk={() => setShowKeys(false)}
onCancel={() => setShowKeys(false)}
title="Keys"
>
<pre>
{keys!.join('\n\n')}
</pre>
</Modal>
<Card
style={{ width: 300, marginTop: 16, alignSelf: 'center' }}
actions={[(
<IconText
key="showkeys"
text="Show keys"
icon={KeyOutlined}
onClick={() => setShowKeys(true)}
/>
), (
<a target="_blank" href={`https://github.com/${user.login}`}>
<IconText
key="gotogithub"
text="Go to Github"
icon={GithubOutlined}
/>
</a>
)]}
>
<Card.Meta
title={user.name}
avatar={(
<Avatar src={user.avatar_url} size={80} />
)}
description={user.location}
/>
</Card>
</>
);
};
export default Profile;

View File

@@ -10,6 +10,10 @@ class Config {
const [user] = this.repo.split('/');
return user;
}
get isProd() {
return process.env.NODE_ENV === 'production';
}
}
export default new Config();

View File

@@ -1,6 +1,7 @@
import React, { useState, useCallback, useContext, createContext } from 'react';
import * as openpgp from 'openpgp';
import { nanoid } from 'nanoid';
import { message } from 'antd';
import GithubContext from './Github';
export interface FileType {
@@ -14,7 +15,7 @@ export interface FileType {
interface EncryptionContextType {
files: {[id: string]: FileType};
addFile: (file: File) => Promise<void>;
addText: (text: string) => Promise<void>;
addText: (text: string, name: string) => Promise<void>;
deleteFile: (id: string) => void;
}
@@ -32,7 +33,7 @@ const encrypt = async (keys: string[], content: string) => {
const encrypted = await openpgp.encrypt({
message,
armor: true,
publicKeys: armoredKeys.reduce((output, key: any) => [...output, ...key.keys], []),
publicKeys: armoredKeys.reduce<any>((output, key: any) => [...output, ...key.keys], []),
});
const { data } = encrypted;
const blob = new Blob([data], {
@@ -55,7 +56,9 @@ const EncryptionProvider: React.FC = ({
});
}, [files]);
const add = (name: string = nanoid()) => {
const add = (name: string) => {
const id = nanoid();
message.info(`Beginning to encrypt ${name}`);
const file: FileType = {
name: `${name}.asc`,
reciever: username,
@@ -63,19 +66,20 @@ const EncryptionProvider: React.FC = ({
};
setFiles(files => ({
...files,
[name]: file,
[id]: file,
}));
const setError = (err: any) => {
console.error(err);
setFiles(files => ({
...files,
[name]: {
...files[name],
[id]: {
...files[id],
status: 'failed',
error: err,
},
}));
message.error(`Failed to encrypt ${name}`);
};
const setContent = (text: string, keys: string[]) => {
@@ -84,12 +88,13 @@ const EncryptionProvider: React.FC = ({
const encrypted = await encrypt(keys, text);
setFiles(files => ({
...files,
[name]: {
...files[name],
[id]: {
...files[id],
link: encrypted,
status: 'encrypted'
},
}));
message.success(`Done encrypting ${name}`);
} catch (err) {
setError(err);
}
@@ -104,7 +109,6 @@ const EncryptionProvider: React.FC = ({
}
const addFile = useCallback(async (file: File) => {
console.log('a', keys, file);
if (!keys) return;
const addedFile = add(file.name);
const reader = new FileReader()
@@ -112,15 +116,14 @@ const EncryptionProvider: React.FC = ({
reader.onabort = addedFile.setError,
reader.onerror = addedFile.setError,
reader.onload = () => {
console.log('foo', file);
addedFile.setContent(reader.result as string, keys);
}
reader.readAsText(file)
}, [keys, username]);
const addText = useCallback(async (text: string) => {
const addText = useCallback(async (text: string, name: string) => {
if (!keys) return;
const file = add();
const file = add(`${name}.txt`);
file.setContent(text, keys);
}, [keys, username]);

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect, createContext } from 'react';
import { Layout, Spin } from 'antd';
interface GithubContextType {
username: string;
@@ -13,6 +14,20 @@ interface Props {
children: React.ReactNode;
}
const Loader = () => (
<Layout
style={{
position: 'fixed',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
width: '100%',
}}
>
<Spin size="large" />
</Layout>
);
const GithubContext = createContext<GithubContextType>({
username: 'unknown',
state: 'failed',
@@ -46,6 +61,10 @@ const GithubProvider: React.FC<Props> = ({
run();
}, [username]);
if (state === 'loading') {
return <Loader />;
}
return (
<GithubContext.Provider
value={{

View File

@@ -1,4 +1,5 @@
import React from 'react';
import 'antd/dist/antd.css';
import { render } from 'react-dom';
import App from './App';

37
src/screens/Debug.tsx Normal file
View File

@@ -0,0 +1,37 @@
import React, { useMemo } from 'react';
import {
Table,
} from 'antd';
import config from '../config';
const columns = [{
title: 'Name',
dataIndex: 'name',
key: 'name',
}, {
title: 'Value',
dataIndex: 'value',
key: 'value',
}];
const Debug: React.FC = () => {
const data = useMemo(() => {
const vals = {
'Repository': config.repo,
'User': config.user,
'Is Production': config.isProd,
};
return Object.entries(vals).map(([name, value]) => ({
key: name,
name,
value: value.toString(),
}));
}, []);
return (
<Table dataSource={data} columns={columns} />
);
};
export default Debug;

View File

@@ -1,16 +1,33 @@
import React, { useContext } from 'react';
import { Collapse, Badge } from 'antd';
import Profile from '../components/Profile';
import Add from '../components/Add';
import FileList from '../components/FileList';
import GithubContext from '../contexts/Github';
import EncryptionContext from '../contexts/Encryption';
const Encrypt: React.FC = () => {
const { username } = useContext(GithubContext);
const { files } = useContext(EncryptionContext);
return (
<div>
<div>To: {username}</div>
<Add />
<FileList />
</div>
<>
<Collapse ghost defaultActiveKey={[2, 3]}>
<Collapse.Panel key={2} header="Encrypt">
<Add />
</Collapse.Panel>
<Collapse.Panel
key={3}
header={(
<Badge count={Object.keys(files).length} offset={[20, 7]}>
Files
</Badge>
)}
>
<FileList />
</Collapse.Panel>
<Collapse.Panel key={1} header="Profile">
<Profile />
</Collapse.Panel>
</Collapse>
</>
);
};