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

22
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Build and Deploy
on: [push]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly.
with:
persist-credentials: false
- name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
run: |
npm install
npm run build
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.5.9
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages # The branch the action should deploy to.
FOLDER: dist # The folder the action should deploy.

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/node_modules
/dist
/.cache

19
babel.config.js Normal file
View File

@@ -0,0 +1,19 @@
const config = (api) => {
api.cache(false);
return {
presets: [
require.resolve('@babel/preset-env'),
require.resolve('@babel/preset-react'),
require.resolve('@babel/preset-typescript'),
],
plugins: [
[require.resolve('babel-plugin-transform-inline-environment-variables'), {
include: [
'GITHUB_REPOSITORY',
],
}],
],
};
};
module.exports = config;

4091
docs/app.js Normal file

File diff suppressed because one or more lines are too long

9
docs/index.html Normal file
View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Webpack App</title>
<meta name="viewport" content="width=device-width, initial-scale=1"></head>
<body>
<script src="app.js"></script></body>
</html>

41
package.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "dropbox",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "webpack"
},
"devDependencies": {
"@babel/core": "^7.11.1",
"@babel/preset-env": "^7.11.0",
"@babel/preset-react": "^7.10.4",
"@babel/preset-typescript": "^7.10.4",
"@types/html-webpack-plugin": "^3.2.3",
"@types/openpgp": "^4.4.12",
"@types/react": "^16.9.46",
"@types/react-dom": "^16.9.8",
"@types/styled-components": "^5.1.2",
"babel-loader": "^8.1.0",
"babel-plugin-transform-inline-environment-variables": "^0.4.3",
"html-webpack-plugin": "^4.3.0",
"parcel-bundler": "^1.12.4",
"ts-node": "^8.10.2",
"typescript": "^3.9.7",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
},
"dependencies": {
"nanoid": "^3.1.12",
"openpgp": "^4.10.7",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-dropzone": "^11.0.3",
"react-feather": "^2.0.8",
"styled-components": "^5.1.1"
},
"browserslist": [
"last 1 Chrome versions"
]
}

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;

11
tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es2018",
"jsx": "react",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}

29
webpack.config.ts Normal file
View File

@@ -0,0 +1,29 @@
import { Configuration } from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import path from 'path';
const config: Configuration = {
mode: 'development',
entry: {
app: [
path.join(__dirname, 'src', 'index.tsx'),
],
},
output: {
path: path.join(__dirname, 'docs'),
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
plugins: [
new HtmlWebpackPlugin(),
],
module: {
rules: [{
test: /\.tsx?/,
use: ['babel-loader'],
}],
},
};
export default config;

7619
yarn.lock Normal file

File diff suppressed because it is too large Load Diff