This commit is contained in:
2020-08-19 16:48:24 +02:00
commit 243e41bb92
23 changed files with 12392 additions and 0 deletions

19
src/App.tsx Normal file
View File

@@ -0,0 +1,19 @@
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { GithubProvider } from './contexts/Github';
import { EncryptionProvider } from './contexts/Encryption';
import Encrypt from './screens/Encrypt';
import theme from './theme';
const App: React.FC = () => (
<ThemeProvider theme={theme}>
<GithubProvider username="morten-olsen">
<div>Test</div>
<EncryptionProvider>
<Encrypt />
</EncryptionProvider>
</GithubProvider>
</ThemeProvider>
);
export default App;

50
src/components/Add.tsx Normal file
View File

@@ -0,0 +1,50 @@
import React, { useState } from 'react';
import styled from 'styled-components';
import { Theme } from '../theme';
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 Add: React.FC = () => {
const [type, setType] = useState<'file' | 'text'>('text');
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>
);
};
export default Add;

View File

@@ -0,0 +1,32 @@
import React, { useContext, useCallback } from 'react';
import styled from 'styled-components';
import { useDropzone } from 'react-dropzone';
import EncryptionContext from '../contexts/Encryption';
import { Upload } from 'react-feather';
const DropWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 50px;
`;
const AddFile: React.FC = () => {
const { addFile } = useContext(EncryptionContext);
const onDrop = useCallback(acceptedFiles => {
acceptedFiles.forEach(addFile);
}, [])
const {getRootProps, getInputProps} = useDropzone({ onDrop });
return (
<div>
<DropWrapper {...getRootProps()}>
<input {...getInputProps()} />
<Upload size={200} />
<p>Drag 'n' drop some files here, or click to select files</p>
</DropWrapper>
</div>
);
};
export default AddFile;

View File

@@ -0,0 +1,41 @@
import React, { useState, useCallback, useContext } from 'react';
import styled from 'styled-components';
import EncryptionContext from '../contexts/Encryption';
import { Theme } from '../theme';
const Wrapper = styled.div`
display: flex;
flex-direction: column;
flex: 1;
`;
const Text = styled.textarea<{
theme: Theme;
}>`
border: none;
height: 200px;
background: transparent;
color: inherit;
padding: ${({ theme }) => theme.margin.medium}px;
font: inherit;
`;
const AddText : React.FC = () => {
const { addText } = useContext(EncryptionContext);
const [text, setText] = useState('');
const add = useCallback(() => {
addText(text);
setText('');
}, [text, addText]);
return (
<Wrapper>
<Text placeholder="Enter you message..." value={text} onChange={evt => setText(evt.target.value)} />
<button onClick={add}>Save</button>
</Wrapper>
);
};
export default AddText;

53
src/components/File.tsx Normal file
View File

@@ -0,0 +1,53 @@
import React from 'react';
import styled from 'styled-components';
import { File } from '../contexts/Encryption';
import { CheckCircle, XCircle, Download, Trash, Loader } from 'react-feather';
import Row, { Cell } from './Row';
interface Props {
remove: () => void;
file: File;
}
const Button = styled.button`
background: none;
border: none;
color: green;
`;
const icons: {[name: string]: typeof CheckCircle} = {
encrypting: Loader,
failed: XCircle,
encrypted: CheckCircle,
};
const FileView: React.FC<Props> = ({
file,
remove,
}) => {
const Icon = icons[file.status];
return (
<Row
left={(
<Cell><Icon /></Cell>
)}
title={file.name}
body={`encrypted for ${file.reciever}`}
right={!!file.link && (
<>
<Cell>
<a target="_blank" href={file.link}>
<Button><Download /></Button>
</a>
</Cell>
<Cell>
<Button onClick={remove}><Trash /></Button>
</Cell>
</>
)}
/>
);
};
export default FileView;

View File

@@ -0,0 +1,21 @@
import React, { useContext } from 'react';
import EncryptionContext from '../contexts/Encryption';
import File from './File';
const Encrypt: React.FC = () => {
const { files, deleteFile } = useContext(EncryptionContext);
return (
<div>
{Object.entries(files).map(([id, file]) => (
<File
key={id}
file={file}
remove={() => deleteFile(id)}
/>
))}
</div>
);
};
export default Encrypt;

58
src/components/Row.tsx Normal file
View File

@@ -0,0 +1,58 @@
import React from 'react';
import styled from 'styled-components';
import { Theme } from '../theme';
interface Props {
left?: React.ReactNode;
right?: React.ReactNode;
title?: string;
body?: string;
children?: React.ReactNode;
}
const Cell = styled.div<{ theme: Theme }>`
padding: ${({ theme }) => theme.margin.medium / 2}px;
align-items: center;
`;
const Wrapper = styled(Cell)`
display: flex;
flex-direction: row;
background: #dfe6e9;
margin-top: ${({ theme }) => theme.margin.medium}px;
`;
const Title = styled.h2`
padding: 0px;
font-size: 22px;
font-weight: bold;
`
const Main = styled(Cell)`
flex: 1;
justify-content: flex-start;
`;
const Row: React.FC<Props> = ({
left,
right,
title,
body,
children,
}) => (
<Wrapper style={{ display: 'flex' }}>
{left}
<Main>
{title && <Title>{title}</Title>}
{body}
{children}
</Main>
{right}
</Wrapper>
);
export {
Cell,
};
export default Row;

15
src/config.ts Normal file
View File

@@ -0,0 +1,15 @@
class Config {
get repo() {
if (!process.env.GITHUB_REPOSITORY) {
return 'morten-olsen/foobar';
}
return process.env.GITHUB_REPOSITORY;
}
get user() {
const [user] = this.repo.split('/');
return user;
}
}
export default new Config();

146
src/contexts/Encryption.tsx Normal file
View File

@@ -0,0 +1,146 @@
import React, { useState, useCallback, useContext, createContext } from 'react';
import * as openpgp from 'openpgp';
import { nanoid } from 'nanoid';
import GithubContext from './Github';
export interface FileType {
name: string;
reciever: string;
status: 'encrypting' | 'failed' | 'encrypted';
error?: any;
link?: string;
}
interface EncryptionContextType {
files: {[id: string]: FileType};
addFile: (file: File) => Promise<void>;
addText: (text: string) => Promise<void>;
deleteFile: (id: string) => void;
}
const EncryptionContext = createContext<EncryptionContextType>({
files: {},
addFile: async () => { throw new Error('Not using provider'); },
addText: async () => { throw new Error('Not using provider'); },
deleteFile: async () => { throw new Error('Not using provider'); },
});
const encrypt = async (keys: string[], content: string) => {
const armoredKeys = await Promise.all(keys.map(openpgp.key.readArmored));
console.log(armoredKeys);
const message = openpgp.message.fromText(content);
const encrypted = await openpgp.encrypt({
message,
armor: true,
publicKeys: armoredKeys.reduce((output, key: any) => [...output, ...key.keys], []),
});
const { data } = encrypted;
const blob = new Blob([data], {
type: 'text/text',
});
const url = URL.createObjectURL(blob);
return url;
};
const EncryptionProvider: React.FC = ({
children,
}) => {
const { username, keys } = useContext(GithubContext);
const [files, setFiles] = useState<EncryptionContextType['files']>({});
const deleteFile = useCallback((id: string) => {
delete files[id];
setFiles({
...files,
});
}, [files]);
const add = (name: string = nanoid()) => {
const file: FileType = {
name: `${name}.asc`,
reciever: username,
status: 'encrypting',
};
setFiles(files => ({
...files,
[name]: file,
}));
const setError = (err: any) => {
console.error(err);
setFiles(files => ({
...files,
[name]: {
...files[name],
status: 'failed',
error: err,
},
}));
};
const setContent = (text: string, keys: string[]) => {
const run = async () => {
try {
const encrypted = await encrypt(keys, text);
setFiles(files => ({
...files,
[name]: {
...files[name],
link: encrypted,
status: 'encrypted'
},
}));
} catch (err) {
setError(err);
}
};
run();
};
return {
setContent,
setError,
};
}
const addFile = useCallback(async (file: File) => {
console.log('a', keys, file);
if (!keys) return;
const addedFile = add(file.name);
const reader = new FileReader()
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) => {
if (!keys) return;
const file = add();
file.setContent(text, keys);
}, [keys, username]);
return (
<EncryptionContext.Provider
value={{
files,
addFile,
addText,
deleteFile,
}}
>
{children}
</EncryptionContext.Provider>
);
};
export {
EncryptionProvider,
};
export default EncryptionContext;

68
src/contexts/Github.tsx Normal file
View File

@@ -0,0 +1,68 @@
import React, { useState, useEffect, createContext } from 'react';
interface GithubContextType {
username: string;
user?: any;
keys?: string[];
error?: any;
state: 'loading' | 'ready' | 'failed'
}
interface Props {
username: string;
children: React.ReactNode;
}
const GithubContext = createContext<GithubContextType>({
username: 'unknown',
state: 'failed',
});
const GithubProvider: React.FC<Props> = ({
username,
children,
}) => {
const [keys, setKeys] = useState<GithubContextType['keys'] | undefined>();
const [state, setState] = useState<GithubContextType['state']>('loading');
const [error, setError] = useState<GithubContextType['state'] | undefined>();
const [user, setUser] = useState<GithubContextType['user'] | undefined>();
useEffect(() => {
const run = async () => {
try {
const keysRes = await fetch(`https://api.github.com/users/${username}/gpg_keys`);
const userRes = await fetch(`https://api.github.com/users/${username}`);
const keys = await keysRes.json();
const user = await userRes.json();
setState('ready');
setKeys(keys.map((key: any) => key.raw_key));
setUser(user);
} catch (err) {
setState('failed');
setError(err);
}
};
run();
}, [username]);
return (
<GithubContext.Provider
value={{
username,
user,
keys,
state,
error,
}}
>
{children}
</GithubContext.Provider>
);
};
export {
GithubProvider,
};
export default GithubContext;

12
src/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="index.tsx"></script>
</body>
</html>

7
src/index.tsx Normal file
View File

@@ -0,0 +1,7 @@
import React from 'react';
import { render } from 'react-dom';
import App from './App';
const root = document.createElement('div');
document.body.appendChild(root);
render(<App />, root);

17
src/screens/Encrypt.tsx Normal file
View File

@@ -0,0 +1,17 @@
import React, { useContext } from 'react';
import Add from '../components/Add';
import FileList from '../components/FileList';
import GithubContext from '../contexts/Github';
const Encrypt: React.FC = () => {
const { username } = useContext(GithubContext);
return (
<div>
<div>To: {username}</div>
<Add />
<FileList />
</div>
);
};
export default Encrypt;

9
src/theme/index.tsx Normal file
View File

@@ -0,0 +1,9 @@
const theme = {
margin: {
medium: 16,
},
};
export type Theme = typeof theme;
export default theme;