├── src
├── pages
│ ├── SignIn.css
│ ├── ImportFeed.css
│ ├── NewChannel.css
│ ├── InviteRequest.css
│ ├── DeleteChannel.css
│ ├── Channel.css
│ ├── DeleteChannel.js
│ ├── NewChannel.js
│ ├── ImportFeed.js
│ ├── InviteRequest.js
│ ├── SignIn.js
│ └── Channel.js
├── App.css
├── components
│ ├── MessageList.css
│ ├── SelectIdentity.css
│ ├── ui
│ │ ├── Icon.js
│ │ ├── Button.js
│ │ ├── Button.css
│ │ ├── File.js
│ │ └── File.css
│ ├── UserList.css
│ ├── Notifications.css
│ ├── UserList.js
│ ├── SelectIdentity.js
│ ├── Message.css
│ ├── ChannelList.css
│ ├── MessageList.js
│ ├── Notifications.js
│ ├── Message.js
│ ├── Compose.css
│ ├── ChannelList.js
│ └── Compose.js
├── App.test.js
├── layouts
│ ├── FullScreen.css
│ ├── FullScreen.js
│ ├── Channel.css
│ └── Channel.js
├── images
│ ├── icon.js
│ ├── cabal-emoji-picker.svg
│ ├── new-channel.svg
│ ├── new-channel-active.svg
│ ├── down-arrow.svg
│ └── download.svg
├── electron
│ ├── preload.js
│ ├── menu.js
│ ├── main.js
│ └── network.js
├── index.js
├── redux
│ ├── store.js
│ ├── commands.js
│ ├── utils.test.js
│ ├── reducers.test.js
│ ├── utils.js
│ ├── network.js
│ └── reducers.js
├── utils.js
├── index.css
└── App.js
├── .env
├── assets
└── icon.icns
├── public
├── favicon.png
├── electron.js
└── index.html
├── Artwork
├── desktop-demo.gif
├── logo-512x512.png
├── logo-1024x1024.png
├── desktop-screenshot.png
├── banner-slim-1280x360.png
├── avatar-twitter-640x640.png
├── banner-github-1280x640.png
├── banner-twitter-1500x500.png
├── cabal-emoji-picker.svg
├── new-channel.svg
├── new-channel-active.svg
├── logo.svg
└── avatar-twitter.svg
├── CODE_OF_CONDUCT.md
├── .gitignore
├── .eslintrc.js
├── README.md
└── package.json
/src/pages/SignIn.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/ImportFeed.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | INLINE_RUNTIME_CHUNK=false
2 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .error {
2 | color: red;
3 | text-align: center;
4 | }
5 |
--------------------------------------------------------------------------------
/assets/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/assets/icon.icns
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/public/favicon.png
--------------------------------------------------------------------------------
/src/pages/NewChannel.css:
--------------------------------------------------------------------------------
1 | .new-channel-form .new-channel-note {
2 | margin-top: 0.75rem;
3 | }
4 |
--------------------------------------------------------------------------------
/Artwork/desktop-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/desktop-demo.gif
--------------------------------------------------------------------------------
/Artwork/logo-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/logo-512x512.png
--------------------------------------------------------------------------------
/Artwork/logo-1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/logo-1024x1024.png
--------------------------------------------------------------------------------
/Artwork/desktop-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/desktop-screenshot.png
--------------------------------------------------------------------------------
/Artwork/banner-slim-1280x360.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/banner-slim-1280x360.png
--------------------------------------------------------------------------------
/Artwork/avatar-twitter-640x640.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/avatar-twitter-640x640.png
--------------------------------------------------------------------------------
/Artwork/banner-github-1280x640.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/banner-github-1280x640.png
--------------------------------------------------------------------------------
/Artwork/banner-twitter-1500x500.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peerlinks/peerlinks-desktop/HEAD/Artwork/banner-twitter-1500x500.png
--------------------------------------------------------------------------------
/src/components/MessageList.css:
--------------------------------------------------------------------------------
1 | .message-list {
2 | flex-grow: 1;
3 | padding: 0.75rem;
4 |
5 | overflow-y: auto;
6 | }
7 |
--------------------------------------------------------------------------------
/public/electron.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | // NOTE: we are in ./build
4 | const esmRequire = require('esm')(module);
5 | esmRequire('../src/electron/main.js');
6 |
--------------------------------------------------------------------------------
/src/pages/InviteRequest.css:
--------------------------------------------------------------------------------
1 | .invite-request-form .invite-request-tips {
2 | margin-top: 1rem;
3 | }
4 |
5 | .invite-request-form .invite-request-tips ul {
6 | margin: 0;
7 | }
8 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | * [Node.js Code of Conduct](https://github.com/nodejs/admin/blob/master/CODE_OF_CONDUCT.md)
4 | * [Node.js Moderation Policy](https://github.com/nodejs/admin/blob/master/Moderation-Policy.md)
5 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node, mocha */
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import App from './App';
5 |
6 | it('renders without crashing', () => {
7 | const div = document.createElement('div');
8 | ReactDOM.render(, div);
9 | ReactDOM.unmountComponentAtNode(div);
10 | });
11 |
--------------------------------------------------------------------------------
/src/components/SelectIdentity.css:
--------------------------------------------------------------------------------
1 | .form-select {
2 | position: relative;
3 | }
4 |
5 | .form-select .form-select-hidden {
6 | position: absolute;
7 | width: 100%;
8 | height: 100%;
9 | top: 0;
10 | left: 0;
11 | opacity: 0;
12 | }
13 |
14 | .form-select .form-select-hidden select {
15 | width: 100%;
16 | height: 100%;
17 | }
18 |
--------------------------------------------------------------------------------
/src/layouts/FullScreen.css:
--------------------------------------------------------------------------------
1 | .full-screen-layout-container {
2 | display: flex;
3 | height: 100%;
4 | width: 100%;
5 |
6 | flex-direction: column;
7 | justify-content: center;
8 | text-align: center;
9 | }
10 |
11 | .full-screen-layout-container .full-screen-layout {
12 | display: block;
13 | margin: 0 auto;
14 | max-width: 32rem;
15 | padding: 0.5rem;
16 | }
17 |
--------------------------------------------------------------------------------
/src/images/icon.js:
--------------------------------------------------------------------------------
1 | import emoji from './cabal-emoji-picker.svg';
2 | import downArrow from './down-arrow.svg';
3 | import download from './download.svg';
4 | import newChannel from './new-channel.svg';
5 | import newChannelActive from './new-channel-active.svg';
6 |
7 | export default {
8 | emoji,
9 | downArrow,
10 | download,
11 | newChannel,
12 | newChannelActive,
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/ui/Icon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import icons from '../../images/icon';
5 |
6 | // todo (tony-go) : add color prop;
7 |
8 | const Icon = ({ iconName, ...rest }) =>
;
9 |
10 | Icon.propTypes = {
11 | iconName: PropTypes.oneOf(Object.keys(icons)).isRequired,
12 | };
13 |
14 | export default Icon;
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 | /dist
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
26 | # editors
27 | /.vscode
28 |
--------------------------------------------------------------------------------
/Artwork/cabal-emoji-picker.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/images/cabal-emoji-picker.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/electron/preload.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | const { ipcRenderer: ipc } = require('electron');
3 |
4 | ipc.on('response', (_, { seq, payload, error, stack }) => {
5 | window.postMessage({ sender: 'preload', seq, payload, error, stack });
6 | });
7 |
8 | window.addEventListener('message', ({ data: message }) => {
9 | if (message.sender === 'preload') {
10 | return;
11 | }
12 |
13 | const { type, seq, payload } = message;
14 | ipc.send(type, { seq, payload });
15 | });
16 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import { ConnectedRouter } from 'connected-react-router';
5 |
6 | import './index.css';
7 | import App from './App';
8 | import createStore from './redux/store';
9 |
10 | const { store, history } = createStore();
11 |
12 | ReactDOM.render(
13 |
14 |
15 |
16 | , document.getElementById('root'));
17 |
--------------------------------------------------------------------------------
/src/layouts/FullScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import './FullScreen.css';
5 |
6 | export default function FullScreen({ children }) {
7 | return
8 |
9 | {children}
10 |
11 |
;
12 | }
13 |
14 | FullScreen.propTypes = {
15 | children: PropTypes.oneOfType([
16 | PropTypes.element,
17 | PropTypes.arrayOf(PropTypes.element.isRequired),
18 | ]),
19 | };
20 |
--------------------------------------------------------------------------------
/src/pages/DeleteChannel.css:
--------------------------------------------------------------------------------
1 | .delete-channel .delete-channel-row {
2 | margin-bottom: 0.5rem;
3 | }
4 |
5 | .delete-channel .delete-channel-name {
6 | border-radius: 0.25rem;
7 | padding: 1rem 0.25rem;
8 | background: #F9F9F9;
9 |
10 | font-size: 1.75rem;
11 | font-weight: bold;
12 | text-overflow: ellipsis;
13 | white-space: nowrap;
14 | overflow: hidden;
15 | }
16 |
17 | .delete-channel .button {
18 | font-size: 1.5rem;
19 | line-height: 2rem;
20 | }
21 |
22 | .delete-channel .delete-button {
23 | margin-right: 0.5rem;
24 | }
25 |
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { createHashHistory as createHistory } from 'history';
2 | import { applyMiddleware, compose, createStore } from 'redux';
3 | import { routerMiddleware } from 'connected-react-router';
4 | import logger from 'redux-logger';
5 | import thunk from 'redux-thunk';
6 |
7 | import createRootReducer from './reducers';
8 |
9 | export default () => {
10 | const history = createHistory();
11 |
12 | const store = createStore(
13 | createRootReducer(history),
14 | compose(applyMiddleware(routerMiddleware(history), thunk, logger)))
15 |
16 | return { store, history };
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/UserList.css:
--------------------------------------------------------------------------------
1 | .user-list {
2 | height: 100%;
3 | background: #f9f9f9;
4 |
5 | overflow-y: auto;
6 | }
7 |
8 | .user-list .user-list-title {
9 | width: 100%;
10 | padding: 0.25rem 0.5rem;
11 | margin-bottom: 0.5rem;
12 | border-bottom: 1px solid #E3E3E3;
13 |
14 | font-weight: bold;
15 | font-size: 1.25rem;
16 | }
17 |
18 | .user-list .user-list-elem {
19 | display: block;
20 |
21 | margin-bottom: 0.25rem;
22 | margin-left: 0.5rem;
23 | font-size: 1rem;
24 | font-weight: bold;
25 |
26 | text-overflow: ellipsis;
27 | white-space: nowrap;
28 | overflow: hidden;
29 | }
30 |
--------------------------------------------------------------------------------
/src/layouts/Channel.css:
--------------------------------------------------------------------------------
1 | .channel-layout {
2 | display: flex;
3 | flex-direction: row;
4 |
5 | height: 100%;
6 | }
7 |
8 | .channel-layout .channel-layout-sidebar {
9 | flex: 0 1 16rem;
10 | border-right: 1px #090E20 solid;
11 |
12 | background: #0B132B;
13 | color: #797e8b;
14 |
15 | height: 100%;
16 | overflow-y: auto;
17 | }
18 |
19 | .channel-layout .channel-layout-sidebar .title {
20 | margin: 0.5rem 0 1rem 0.5rem;
21 |
22 | font-size: 1.5rem;
23 | font-weight: bold;
24 | line-height: 2rem;
25 | }
26 |
27 | .channel-layout .channel-layout-main {
28 | flex: 1 1 32rem;
29 | overflow: hidden;
30 | }
31 |
--------------------------------------------------------------------------------
/src/images/new-channel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/images/new-channel-active.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/redux/commands.js:
--------------------------------------------------------------------------------
1 | import {
2 | invite, acceptInvite, displayHelp, displayFeedURL, renameIdentityPair,
3 | displayPeerID,
4 | } from './actions';
5 |
6 | export default new Map([
7 | [
8 | 'help',
9 | { args: [ ], action: displayHelp },
10 | ],
11 | [
12 | 'invite',
13 | { args: [ 'inviteeName', 'request' ], action: invite },
14 | ],
15 | [
16 | 'accept-invite',
17 | { args: [ 'requestId', 'box' ], action: acceptInvite },
18 | ],
19 | [
20 | 'get-feed-url',
21 | { args: [ ], action: displayFeedURL },
22 | ],
23 | [
24 | 'rename',
25 | { args: [ 'newName' ], action: renameIdentityPair },
26 | ],
27 | [
28 | 'get-peer-id',
29 | { args: [ ], action: displayPeerID },
30 | ],
31 | ]);
32 |
33 |
--------------------------------------------------------------------------------
/src/layouts/Channel.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import ChannelList from '../components/ChannelList';
5 | import Notifications from '../components/Notifications';
6 |
7 | import './Channel.css';
8 |
9 | export default function ChannelLayout({ children }) {
10 | return
11 |
12 |
13 |
16 |
17 |
18 | {children}
19 |
20 |
;
21 | }
22 |
23 | ChannelLayout.propTypes = {
24 | children: PropTypes.oneOfType([
25 | PropTypes.element,
26 | PropTypes.arrayOf(PropTypes.element.isRequired),
27 | ]),
28 | };
29 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 | PeerLinks
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | export function keyToColor(publicKey) {
2 | let r = parseInt(publicKey.slice(0, 2), 16);
3 | let g = parseInt(publicKey.slice(2, 4), 16);
4 | let b = parseInt(publicKey.slice(4, 6), 16);
5 |
6 | const scale = 255 / (Math.sqrt(r ** 2 + g ** 2 + b ** 2) + 1e-23);
7 | r *= scale;
8 | g *= scale;
9 | b *= scale;
10 |
11 | return `rgb(${r},${g},${b})`;
12 | }
13 |
14 | export function prerenderUserName({ name, publicKey, isInternal = false }) {
15 | if (!isInternal) {
16 | name = name.trim().replace(/^[#@]+/, '');
17 | }
18 |
19 | return {
20 | name,
21 | color: keyToColor(publicKey),
22 | };
23 | }
24 |
25 | export function getFeedURL(feed) {
26 | return `peerlinks://feed/${feed.publicKeyB58}?` +
27 | `name=${encodeURIComponent(feed.name)}`;
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/ui/Button.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 |
5 | // todo (tony-go) : add iconName Props and render Icon component
6 | // todo (tony-go) : replace all buttons in the app
7 |
8 | import './Button.css';
9 |
10 | const Button = ({ onClick, color, label, ...rest }) =>
11 | ;
21 | Button.propTypes = {
22 | onClick: PropTypes.func.isRequired,
23 | color: PropTypes.oneOf([
24 | 'success',
25 | 'danger',
26 | ]),
27 | label: PropTypes.string.isRequired,
28 | };
29 |
30 | Button.defaultProps = {
31 | color: 'black',
32 | };
33 |
34 | export default Button;
35 |
--------------------------------------------------------------------------------
/src/components/ui/Button.css:
--------------------------------------------------------------------------------
1 | .button {
2 | border: 1px solid #4c4c9d;
3 | border-radius: 0.25rem;
4 | padding: 0.5rem;
5 | background: none;
6 | text-decoration:none;
7 | color: #4c4c9d;
8 | font-size: 1rem;
9 |
10 | cursor: pointer;
11 | transition: background 0.25s, color 0.25s;
12 | }
13 |
14 | .button:focus {
15 | outline: 0;
16 | }
17 |
18 | .button:disabled {
19 | color: #afafaf;
20 | }
21 |
22 | .button:hover:not(:disabled), .button:focus:not(:disabled) {
23 | background: #4c4c9d;
24 | color: #f9f9f9;
25 | }
26 |
27 | /* Types : danger, success */
28 |
29 | .button.danger {
30 | background: none;
31 | color: #ff3c38;
32 | border-color: #ff3c38;
33 | }
34 |
35 | .button.danger:hover:not(:disabled) {
36 | background: #ff3c38;
37 | color: #fbf5f3;
38 | }
39 |
40 | .button.success {
41 | background: none;
42 | color: forestgreen;
43 | border-color: forestgreen;
44 | }
45 |
46 | .button.success:hover:not(:disabled) {
47 | background: forestgreen;
48 | color: #fbf5f3;
49 | }
50 |
--------------------------------------------------------------------------------
/src/redux/utils.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node, mocha */
2 | import * as assert from 'assert';
3 |
4 | import { appendMessage } from './utils';
5 |
6 | it('appends message to empty list', () => {
7 | const original = [];
8 | const next = original.slice();
9 | appendMessage(next, { hash: 'a', height: 1 });
10 |
11 | // No modifications
12 | assert.strictEqual(original.length, 0);
13 |
14 | // Appends
15 | assert.strictEqual(next.length, 1);
16 | assert.strictEqual(next[0].hash, 'a');
17 | });
18 |
19 | it('sorts by height and then by hash', () => {
20 | const list = [];
21 | appendMessage(list, { hash: 'a', height: 1 });
22 | appendMessage(list, { hash: 'b', height: 1 });
23 | appendMessage(list, { hash: 'a', height: 2 });
24 | appendMessage(list, { hash: 'c', height: 3 });
25 | appendMessage(list, { hash: 'a', height: 3 });
26 | appendMessage(list, { hash: 'x', height: 0 });
27 |
28 | assert.deepStrictEqual(list.map((msg) => `${msg.height}:${msg.hash}`), [
29 | '0:x',
30 | '1:a',
31 | '1:b',
32 | '2:a',
33 | '3:a',
34 | '3:c',
35 | ]);
36 | });
37 |
--------------------------------------------------------------------------------
/src/components/Notifications.css:
--------------------------------------------------------------------------------
1 | .notification-container {
2 | position: fixed;
3 | z-index: 100;
4 | top: 0;
5 | width: 100%;
6 | }
7 |
8 | .notification-container .notification-list {
9 | flex: 0 0 0;
10 | }
11 |
12 | .notification-container .notification-rest {
13 | flex: 1 0 0;
14 | }
15 |
16 | .notification-list .notification {
17 | padding: 0.25rem 0.5rem;
18 | height: 3rem;
19 | line-height: 2.5rem;
20 | overflow-y: hidden;
21 |
22 | display: flex;
23 | flex-direction: row;
24 | transition: 0.25 height;
25 | }
26 |
27 | .notification-list .notification {
28 | background: #fbf5f3;
29 | border-bottom: 1px solid #222;
30 | color: #222;
31 | }
32 |
33 | .notification-list .notification-error {
34 | background: #b14643;
35 | border-bottom: 1px solid #582322;
36 | color: #fbf5f3;
37 | }
38 |
39 | .notification-list .notification .notification-content {
40 | flex-grow: 1;
41 | }
42 |
43 | .notification-list .notification .notification-dismiss-container {
44 | flex-grow: 0;
45 | }
46 |
47 | .notification-list .notification .notification-dismiss {
48 | height: 2.5rem;
49 | }
50 |
51 | .notification-list .notification-error .notification-dismiss {
52 | background: #fbf5f3;
53 | }
54 |
--------------------------------------------------------------------------------
/src/images/down-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
45 |
--------------------------------------------------------------------------------
/src/components/UserList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { prerenderUserName } from '../utils';
5 |
6 | import './UserList.css';
7 |
8 | export default function UserList({ channelName, users }) {
9 | const renderUser = (user, index) => {
10 | if (user.displayPath.length === 0) {
11 | return #{channelName}
;
12 | }
13 |
14 | const { name, color } = prerenderUserName({
15 | name: user.displayPath[user.displayPath.length - 1],
16 | publicKey: user.publicKeys[user.publicKeys.length - 1],
17 | });
18 |
19 | const style = { color };
20 | return
21 | {name}
22 |
;
23 | };
24 |
25 | return
26 |
27 | Active peers:
28 |
29 | {users.map(renderUser)}
30 |
;
31 | }
32 |
33 | UserList.propTypes = {
34 | channelName: PropTypes.string.isRequired,
35 | users: PropTypes.arrayOf(PropTypes.shape({
36 | displayPath: PropTypes.arrayOf(PropTypes.string.isRequired),
37 | publicKeys: PropTypes.arrayOf(PropTypes.string.isRequired),
38 | })),
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/SelectIdentity.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import './SelectIdentity.css';
5 |
6 | export default function SelectIdentity(props) {
7 | const { children } = props;
8 |
9 | const onChange = (e) => {
10 | e.preventDefault();
11 | props.onChange(e.target.value);
12 | };
13 |
14 | const active = children.find((child) => child.props.value === props.value);
15 |
16 | return
17 |
18 | {active && active.props.label}
19 |
20 |
21 |
24 |
25 |
;
26 | }
27 |
28 | SelectIdentity.propTypes = {
29 | className: PropTypes.string,
30 | onChange: PropTypes.func.isRequired,
31 | value: PropTypes.string,
32 |
33 | children: PropTypes.arrayOf(PropTypes.shape({
34 | props: PropTypes.object.isRequired,
35 | })),
36 | };
37 |
38 | export const Option = ({ value, label }) => {
39 | return