mirror of
https://github.com/morten-olsen/parcel.git
synced 2026-02-08 01:36:24 +01:00
init
This commit is contained in:
19
src/App.tsx
Normal file
19
src/App.tsx
Normal 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
50
src/components/Add.tsx
Normal 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;
|
||||
32
src/components/AddFile.tsx
Normal file
32
src/components/AddFile.tsx
Normal 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;
|
||||
41
src/components/AddText.tsx
Normal file
41
src/components/AddText.tsx
Normal 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
53
src/components/File.tsx
Normal 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;
|
||||
21
src/components/FileList.tsx
Normal file
21
src/components/FileList.tsx
Normal 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
58
src/components/Row.tsx
Normal 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
15
src/config.ts
Normal 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
146
src/contexts/Encryption.tsx
Normal 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
68
src/contexts/Github.tsx
Normal 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
12
src/index.html
Normal 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
7
src/index.tsx
Normal 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
17
src/screens/Encrypt.tsx
Normal 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
9
src/theme/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
const theme = {
|
||||
margin: {
|
||||
medium: 16,
|
||||
},
|
||||
};
|
||||
|
||||
export type Theme = typeof theme;
|
||||
|
||||
export default theme;
|
||||
Reference in New Issue
Block a user