init push - laying out the project
This commit is contained in:
24
portal/client/packs/application.js
Normal file
24
portal/client/packs/application.js
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import { fab } from '@fortawesome/free-brands-svg-icons';
|
||||
import { fas } from '@fortawesome/free-solid-svg-icons';
|
||||
import { far } from '@fortawesome/free-regular-svg-icons';
|
||||
import TimeAgo from 'javascript-time-ago';
|
||||
import en from 'javascript-time-ago/locale/en.json';
|
||||
|
||||
import '../src/assets/style';
|
||||
|
||||
import EntryPoint from '../src/entry_point';
|
||||
|
||||
library.add(fas, fab, far);
|
||||
|
||||
TimeAgo.addDefaultLocale(en);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const container = document.getElementById('app');
|
||||
const root = createRoot(container);
|
||||
root.render(<EntryPoint />);
|
||||
});
|
5
portal/client/src/assets/images.js
Normal file
5
portal/client/src/assets/images.js
Normal file
@ -0,0 +1,5 @@
|
||||
import MAIN_BACKGROUND_IMG from './images/main-bg.jpg';
|
||||
import LOGO from './images/logo.png';
|
||||
import LINKEDIN from './images/linkedin.png';
|
||||
|
||||
export { MAIN_BACKGROUND_IMG, LINKEDIN, LOGO };
|
BIN
portal/client/src/assets/images/iconmonstr-arrow-65-240.png
Normal file
BIN
portal/client/src/assets/images/iconmonstr-arrow-65-240.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
BIN
portal/client/src/assets/images/linkedin.png
Normal file
BIN
portal/client/src/assets/images/linkedin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.3 KiB |
BIN
portal/client/src/assets/images/logo.png
Normal file
BIN
portal/client/src/assets/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.0 KiB |
4
portal/client/src/assets/images/logo.svg
Normal file
4
portal/client/src/assets/images/logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.1 KiB |
BIN
portal/client/src/assets/images/main-bg.jpg
Normal file
BIN
portal/client/src/assets/images/main-bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 109 KiB |
9
portal/client/src/assets/style.js
Normal file
9
portal/client/src/assets/style.js
Normal file
@ -0,0 +1,9 @@
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'react-datetime/css/react-datetime.css';
|
||||
import 'react-date-range/dist/styles.css';
|
||||
import 'react-date-range/dist/theme/default.css';
|
||||
import 'react-input-range/lib/css/index.css';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import 'react-confirm-alert/src/react-confirm-alert.css';
|
||||
|
||||
import './stylesheets/style.scss';
|
1210
portal/client/src/assets/stylesheets/style.scss
Normal file
1210
portal/client/src/assets/stylesheets/style.scss
Normal file
File diff suppressed because it is too large
Load Diff
6
portal/client/src/channels/consumer.js
Normal file
6
portal/client/src/channels/consumer.js
Normal file
@ -0,0 +1,6 @@
|
||||
// Action Cable provides the framework to deal with WebSockets in Rails.
|
||||
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
|
||||
|
||||
import { createConsumer } from '@rails/actioncable';
|
||||
|
||||
export default createConsumer();
|
15
portal/client/src/data/axiosClient.js
Normal file
15
portal/client/src/data/axiosClient.js
Normal file
@ -0,0 +1,15 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const axiosObj = () => {
|
||||
const instance = axios.create({
|
||||
headers: {
|
||||
'cache-control': 'no-cache',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').content,
|
||||
},
|
||||
});
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
export default axiosObj();
|
19
portal/client/src/data/concerns/filterable.js
Normal file
19
portal/client/src/data/concerns/filterable.js
Normal file
@ -0,0 +1,19 @@
|
||||
import {action, observable} from 'mobx';
|
||||
|
||||
import Pagination from '../entities/pagination';
|
||||
|
||||
const Filterable = {
|
||||
pagination: null,
|
||||
|
||||
initializeFilterable: action(function(params) {
|
||||
this.pagination = new Pagination(
|
||||
{
|
||||
page: 1,
|
||||
params,
|
||||
},
|
||||
this
|
||||
);
|
||||
}),
|
||||
};
|
||||
|
||||
export default Filterable;
|
8
portal/client/src/data/concerns/notfications.js
Normal file
8
portal/client/src/data/concerns/notfications.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const notification = {
|
||||
notifySuccess: message => toast(message, { type: 'success' }),
|
||||
notifyError: message => toast(message, { type: 'error' }),
|
||||
};
|
||||
|
||||
export default notification;
|
67
portal/client/src/data/entities/base_entity.js
Normal file
67
portal/client/src/data/entities/base_entity.js
Normal file
@ -0,0 +1,67 @@
|
||||
import {
|
||||
action,
|
||||
computed,
|
||||
extendObservable,
|
||||
flow,
|
||||
makeObservable,
|
||||
observable,
|
||||
} from 'mobx';
|
||||
import { camelCase, isEmpty, map } from 'lodash';
|
||||
|
||||
import client from '../axiosClient';
|
||||
import notification from '../concerns/notfications';
|
||||
|
||||
class BaseEntity {
|
||||
/* eslint-disable */
|
||||
store;
|
||||
client;
|
||||
@observable dirty = false;
|
||||
@observable json = false;
|
||||
/* eslint-enable */
|
||||
|
||||
constructor(value, store) {
|
||||
makeObservable(this);
|
||||
|
||||
this.store = store;
|
||||
this.client = client;
|
||||
this.json = value;
|
||||
|
||||
extendObservable(this, notification);
|
||||
}
|
||||
|
||||
@computed
|
||||
get currentUser() {
|
||||
return this.store.rootStore.userStore.currentUser;
|
||||
}
|
||||
|
||||
@action
|
||||
initialize(params) {
|
||||
this.update(params);
|
||||
}
|
||||
|
||||
@action
|
||||
update(params, updateServer) {
|
||||
map(
|
||||
Object.keys(params),
|
||||
function(k) {
|
||||
this[camelCase(k)] = params[k];
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
if (updateServer) this.updateServer(params);
|
||||
}
|
||||
|
||||
@action
|
||||
destroy() {
|
||||
if (isEmpty(this.store)) return;
|
||||
|
||||
this.store.records.splice(this.store.records.indexOf(this), 1);
|
||||
}
|
||||
|
||||
@flow
|
||||
*updateServer(params) {
|
||||
yield this.client.put(this.updateUrl, params);
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseEntity;
|
37
portal/client/src/data/entities/bet.js
Normal file
37
portal/client/src/data/entities/bet.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import BaseEntity from './base_entity';
|
||||
|
||||
class Bet extends BaseEntity {
|
||||
/* eslint-disable */
|
||||
id;
|
||||
outcome;
|
||||
exchange;
|
||||
exchangeEventName;
|
||||
exchangeOdds;
|
||||
tipProviderOdds;
|
||||
stake;
|
||||
expectedValue;
|
||||
marketIdentifier;
|
||||
exchangeMarketDetails;
|
||||
betPlacementType;
|
||||
executedOdds;
|
||||
evPercent;
|
||||
eventScheduledTime;
|
||||
/* eslint-enable */
|
||||
|
||||
constructor(value, store) {
|
||||
super(value, store);
|
||||
|
||||
makeObservable(this);
|
||||
|
||||
this.handleConstruction(value);
|
||||
}
|
||||
|
||||
@action
|
||||
handleConstruction(value) {
|
||||
const val = { ...value };
|
||||
this.initialize(val);
|
||||
}
|
||||
}
|
||||
|
||||
export default Bet;
|
48
portal/client/src/data/entities/exchange_account.js
Normal file
48
portal/client/src/data/entities/exchange_account.js
Normal file
@ -0,0 +1,48 @@
|
||||
import { action, computed, flow, makeObservable, observable } from 'mobx';
|
||||
import BaseEntity from './base_entity';
|
||||
import client from '../axiosClient';
|
||||
|
||||
class ExchangeAccount extends BaseEntity {
|
||||
/* eslint-disable */
|
||||
id;
|
||||
exchangeName;
|
||||
exchangeId;
|
||||
contactEmail;
|
||||
contactName;
|
||||
stakeStrategyName;
|
||||
sourceTypes;
|
||||
stakeStrategyConfig;
|
||||
stakeStrategy;
|
||||
lastLogTime;
|
||||
@observable isActive;
|
||||
@observable canBet;
|
||||
log;
|
||||
|
||||
@observable accountBalance;
|
||||
/* eslint-enable */
|
||||
|
||||
constructor(value, store) {
|
||||
super(value, store);
|
||||
|
||||
makeObservable(this);
|
||||
|
||||
this.handleConstruction(value);
|
||||
}
|
||||
|
||||
@action
|
||||
handleConstruction(value) {
|
||||
const val = { ...value };
|
||||
this.initialize(val);
|
||||
}
|
||||
|
||||
@flow
|
||||
*updateExchangeAccount(params) {
|
||||
this.update(params);
|
||||
yield client.put('/api/v1/exchange_account.json', {
|
||||
exchange_account_id: this.id,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default ExchangeAccount;
|
58
portal/client/src/data/entities/pagination.js
Normal file
58
portal/client/src/data/entities/pagination.js
Normal file
@ -0,0 +1,58 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { isNull } from 'lodash';
|
||||
|
||||
import BaseEntity from './base_entity';
|
||||
|
||||
class Pagination extends BaseEntity {
|
||||
/* eslint-disable */
|
||||
@observable count = 0;
|
||||
@observable page = 1;
|
||||
@observable pages = null;
|
||||
@observable prev = null;
|
||||
@observable next = null;
|
||||
@observable last = null;
|
||||
@observable from = null;
|
||||
@observable to = null;
|
||||
@observable params = {};
|
||||
@observable filterType = 'fetch';
|
||||
initialParams = {};
|
||||
/* eslint-enable */
|
||||
|
||||
constructor(value, store) {
|
||||
super(value, store);
|
||||
|
||||
makeObservable(this);
|
||||
|
||||
this.initialize(value);
|
||||
this.initialParams = value.params;
|
||||
}
|
||||
|
||||
@action
|
||||
fetch() {
|
||||
this.store.fetch({ ...this.params, page: this.page });
|
||||
}
|
||||
|
||||
@action
|
||||
updateParams(params) {
|
||||
this.update({ params: { ...this.params, ...params } });
|
||||
}
|
||||
|
||||
@action
|
||||
gotoPage(page) {
|
||||
this.update({ page });
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
@action
|
||||
resetPageAndFetch() {
|
||||
this.gotoPage(1);
|
||||
}
|
||||
|
||||
@action
|
||||
reset() {
|
||||
this.update({ page: 1, params: this.initialParams });
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
|
||||
export default Pagination;
|
51
portal/client/src/data/entities/user.js
Normal file
51
portal/client/src/data/entities/user.js
Normal file
@ -0,0 +1,51 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import BaseEntity from './base_entity';
|
||||
|
||||
class User extends BaseEntity {
|
||||
/* eslint-disable */
|
||||
id;
|
||||
email;
|
||||
accountId;
|
||||
accountName;
|
||||
@observable status;
|
||||
@observable firstName;
|
||||
@observable lastName;
|
||||
@observable strKey;
|
||||
@observable fullname;
|
||||
@observable onboarded = [];
|
||||
@observable environment = 'production';
|
||||
/* eslint-enable */
|
||||
|
||||
constructor(value, store) {
|
||||
super(value, store);
|
||||
|
||||
makeObservable(this);
|
||||
|
||||
this.handleConstruction(value);
|
||||
}
|
||||
|
||||
updateUrl = () => `/api/v1/users/${this.id}.json`;
|
||||
|
||||
@computed
|
||||
get fullName() {
|
||||
return `${this.firstName} ${this.lastName}`;
|
||||
}
|
||||
|
||||
@computed
|
||||
get asSelectOption() {
|
||||
return { label: this.fullName, value: this.id };
|
||||
}
|
||||
|
||||
@action
|
||||
logout = () => {
|
||||
window.location.href = '/public';
|
||||
};
|
||||
|
||||
@action
|
||||
handleConstruction(value) {
|
||||
const val = { ...value };
|
||||
this.initialize(val);
|
||||
}
|
||||
}
|
||||
|
||||
export default User;
|
15
portal/client/src/data/index.js
Normal file
15
portal/client/src/data/index.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
|
||||
import AppStore from './stores/app_store';
|
||||
import UserStore from './stores/user_store';
|
||||
import BetStore from './stores/bet_store';
|
||||
|
||||
export default class RootStore {
|
||||
constructor() {
|
||||
this.appStore = new AppStore(this);
|
||||
this.userStore = new UserStore(this);
|
||||
this.betStore = new BetStore(this);
|
||||
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
}
|
4
portal/client/src/data/provider.js
Normal file
4
portal/client/src/data/provider.js
Normal file
@ -0,0 +1,4 @@
|
||||
import React from 'react';
|
||||
|
||||
export const StoresContext = React.createContext(null);
|
||||
export const StoreProvider = StoresContext.Provider;
|
7
portal/client/src/data/store.js
Normal file
7
portal/client/src/data/store.js
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { StoresContext } from './provider';
|
||||
|
||||
const useStore = () => React.useContext(StoresContext);
|
||||
|
||||
export default useStore;
|
126
portal/client/src/data/stores/app_store.js
Normal file
126
portal/client/src/data/stores/app_store.js
Normal file
@ -0,0 +1,126 @@
|
||||
import { action, flow, makeObservable, observable } from 'mobx';
|
||||
import BaseStore from './base_store';
|
||||
import client from '../axiosClient';
|
||||
import ExchangeAccount from '../entities/exchange_account';
|
||||
import consumer from '../../channels/consumer';
|
||||
|
||||
class AppStore extends BaseStore {
|
||||
@observable sidebarOpened = true;
|
||||
|
||||
@observable exchangeAccount;
|
||||
|
||||
@observable currentExchangeAccountId;
|
||||
|
||||
@observable totalWinAmount = 0;
|
||||
|
||||
@observable totalLostAmount = 0;
|
||||
|
||||
@observable totalRiskedAmount = 0;
|
||||
|
||||
@observable totalTips = 0;
|
||||
|
||||
@observable totalTipsSkipped = 0;
|
||||
|
||||
@observable totalTipsProcessing = 0;
|
||||
|
||||
@observable totalTipsExpired = 0;
|
||||
|
||||
@observable totalTipsIgnored = 0;
|
||||
|
||||
@observable totalTipsVoided = 0;
|
||||
|
||||
@observable totalPlacedBets = 0;
|
||||
|
||||
@observable totalPlacedBetsWon = 0;
|
||||
|
||||
@observable totalPlacedBetsLost = 0;
|
||||
|
||||
@observable totalPlacedBetsOpen = 0;
|
||||
|
||||
@observable averageOddsWon = 0;
|
||||
|
||||
@observable averageOddsLost = 0;
|
||||
|
||||
@observable averageOdds = 0;
|
||||
|
||||
|
||||
@observable appInitialised = false;
|
||||
|
||||
@observable autoRefreshResults = true;
|
||||
|
||||
@observable runningSince = null;
|
||||
|
||||
@observable exchangeAccounts = [];
|
||||
|
||||
constructor(store) {
|
||||
super(store, null);
|
||||
this.consumer = consumer;
|
||||
makeObservable(this);
|
||||
this.fetchConfigFromServer();
|
||||
}
|
||||
|
||||
@action
|
||||
asPercentOfTotal(val) {
|
||||
if (val === 0 || this.totalPlacedBets === 0) return '0%';
|
||||
return `${Math.round((val / this.totalPlacedBets) * 100)}%`;
|
||||
}
|
||||
|
||||
@flow
|
||||
*fetchConfigFromServer() {
|
||||
const response = yield client.get('/api/v1/app/configuration.json');
|
||||
if (response.data.success) {
|
||||
this.update(response.data.config);
|
||||
}
|
||||
}
|
||||
|
||||
@flow
|
||||
*fetchSummaryFiguresFromServer() {
|
||||
const response = yield client.get('/api/v1/app/summary.json', {
|
||||
params: {
|
||||
exchange_account_id:
|
||||
this.currentExchangeAccountId || this.exchangeAccount?.id || '',
|
||||
},
|
||||
});
|
||||
if (response.data.success) {
|
||||
this.updateSummary(response.data.summary);
|
||||
}
|
||||
}
|
||||
|
||||
updateSummary(summary) {
|
||||
console.log('Refreshing dashboard');
|
||||
this.update(summary);
|
||||
const ea = new ExchangeAccount(summary.exchange_account);
|
||||
this.update({
|
||||
exchangeAccount: ea,
|
||||
appInitialised: true,
|
||||
currentExchangeAccountId: ea.id,
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
console.log('Refreshing filtered results');
|
||||
this.rootStore.betStore.fetch(this.rootStore.betStore.params);
|
||||
return null;
|
||||
}
|
||||
|
||||
startListeningToAccountChannel() {
|
||||
const obj = this;
|
||||
this.consumer.subscriptions.create(
|
||||
{ channel: 'ExchangeAccountChannel', id: obj.exchangeAccount.id },
|
||||
{
|
||||
connected() {
|
||||
console.log('Connected to ExchangeAccountChannel');
|
||||
},
|
||||
disconnected() {
|
||||
console.log('Disconnected from ExchangeAccountChannel');
|
||||
},
|
||||
received(data) {
|
||||
console.log('Cable received');
|
||||
return null;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AppStore;
|
86
portal/client/src/data/stores/base_store.js
Normal file
86
portal/client/src/data/stores/base_store.js
Normal file
@ -0,0 +1,86 @@
|
||||
import {
|
||||
action,
|
||||
computed,
|
||||
extendObservable,
|
||||
makeObservable,
|
||||
observable,
|
||||
} from 'mobx';
|
||||
import {
|
||||
camelCase,
|
||||
filter,
|
||||
find,
|
||||
includes,
|
||||
isEmpty,
|
||||
map,
|
||||
uniqBy,
|
||||
} from 'lodash';
|
||||
import notification from '../concerns/notfications';
|
||||
import client from '../axiosClient';
|
||||
|
||||
class BaseStore {
|
||||
/* eslint-disable */
|
||||
rootStore;
|
||||
client;
|
||||
Entity;
|
||||
@observable records = [];
|
||||
@observable fetching = false;
|
||||
@observable fetched = false;
|
||||
/* eslint-enable */
|
||||
|
||||
constructor(store, entity) {
|
||||
makeObservable(this);
|
||||
|
||||
this.rootStore = store;
|
||||
this.client = client;
|
||||
this.Entity = entity;
|
||||
|
||||
extendObservable(this, notification);
|
||||
}
|
||||
|
||||
getById = id => this.getByParams({ id });
|
||||
|
||||
getByParams = params => find(this.records, params);
|
||||
|
||||
getMultipleByParams = params => filter(this.records, params);
|
||||
|
||||
getMultipleById = ids => filter(this.records, r => includes(ids, r.id));
|
||||
|
||||
@computed
|
||||
get filteredRecords() {
|
||||
return this.records;
|
||||
}
|
||||
|
||||
@computed
|
||||
get hasRecords() {
|
||||
return !isEmpty(this.records);
|
||||
}
|
||||
|
||||
@action
|
||||
initialize(params) {
|
||||
this.update(params);
|
||||
}
|
||||
|
||||
@action
|
||||
update(params) {
|
||||
map(
|
||||
Object.keys(params),
|
||||
function(k) {
|
||||
this[camelCase(k)] = params[k];
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
addRecord(record) {
|
||||
const obj = new this.Entity(record, this);
|
||||
|
||||
this.records = uniqBy(
|
||||
[...this.records, new this.Entity(record, this)],
|
||||
'id'
|
||||
);
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseStore;
|
70
portal/client/src/data/stores/bet_store.js
Normal file
70
portal/client/src/data/stores/bet_store.js
Normal file
@ -0,0 +1,70 @@
|
||||
import {
|
||||
extendObservable,
|
||||
flow,
|
||||
makeObservable,
|
||||
observable,
|
||||
override,
|
||||
} from 'mobx';
|
||||
|
||||
import { map, orderBy } from 'lodash';
|
||||
import BaseStore from './base_store';
|
||||
import Bet from '../entities/bet';
|
||||
import Filterable from '../concerns/filterable';
|
||||
import Pagination from '../entities/pagination';
|
||||
|
||||
class BetStore extends BaseStore {
|
||||
@observable params = {
|
||||
event_name: '',
|
||||
outcome: '',
|
||||
created_at: { from: null, to: null },
|
||||
};
|
||||
|
||||
|
||||
constructor(store) {
|
||||
super(store, Bet);
|
||||
|
||||
extendObservable(this, Filterable);
|
||||
|
||||
this.initializeFilterable(this.params);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@override
|
||||
get filteredRecords() {
|
||||
return orderBy(this.records, ['createdAt'], ['desc']);
|
||||
}
|
||||
|
||||
@flow
|
||||
*fetch(pms) {
|
||||
this.params = pms;
|
||||
const response = yield this.client.get('/api/v1/bets.json', {
|
||||
params: {
|
||||
...this.params,
|
||||
exchange_account_id: this.rootStore.appStore.currentExchangeAccountId,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data.summary) {
|
||||
this.rootStore.appStore.updateSummary(response.data.summary);
|
||||
}
|
||||
|
||||
if (response.data.bets) {
|
||||
this.update({
|
||||
records: map(response.data.bets, bet => this.addRecord(bet)),
|
||||
});
|
||||
}
|
||||
if (response.data.pagy) {
|
||||
const p = new Pagination({ ...response.data.pagy }, this);
|
||||
this.pagination.count = p.count;
|
||||
this.pagination.prev = p.prev;
|
||||
this.pagination.next = p.next;
|
||||
this.pagination.last = p.last;
|
||||
this.pagination.from = p.from;
|
||||
this.pagination.to = p.to;
|
||||
this.pagination.page = p.page;
|
||||
this.pagination.pages = p.pages;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BetStore;
|
36
portal/client/src/data/stores/user_store.js
Normal file
36
portal/client/src/data/stores/user_store.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { computed, flow, makeObservable, observable } from 'mobx';
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import BaseStore from './base_store';
|
||||
import User from '../entities/user';
|
||||
|
||||
class UserStore extends BaseStore {
|
||||
/* eslint-disable */
|
||||
@observable currentUser = {};
|
||||
@observable fetchingCurrentUser = true;
|
||||
/* eslint-enable */
|
||||
|
||||
constructor(store) {
|
||||
super(store, User);
|
||||
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@computed
|
||||
get userSignedIn() {
|
||||
return !isEmpty(this.currentUser);
|
||||
}
|
||||
|
||||
@flow
|
||||
*fetchCurrentUser() {
|
||||
const response = yield this.client.get('/api/v1/users/auth.json');
|
||||
|
||||
if (response.data.success) {
|
||||
this.update({ currentUser: new User(response.data.user, this) });
|
||||
}
|
||||
|
||||
this.fetchingCurrentUser = false;
|
||||
}
|
||||
}
|
||||
|
||||
export default UserStore;
|
22
portal/client/src/entry_point/index.js
Normal file
22
portal/client/src/entry_point/index.js
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
import { BrowserRouter, Switch } from 'react-router-dom';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { StoreProvider } from '../data/provider';
|
||||
import RootStore from '../data';
|
||||
import AppRoutes from '../routes';
|
||||
|
||||
const EntryPoint = () => (
|
||||
<StoreProvider value={new RootStore()}>
|
||||
<BrowserRouter>
|
||||
<div className="main-app">
|
||||
<ToastContainer />
|
||||
<Switch>
|
||||
<AppRoutes />
|
||||
</Switch>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</StoreProvider>
|
||||
);
|
||||
|
||||
export default EntryPoint;
|
66
portal/client/src/helpers/filter_helpers.js
Normal file
66
portal/client/src/helpers/filter_helpers.js
Normal file
@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
DateRange,
|
||||
InputRangeTag,
|
||||
ReactSelectTag,
|
||||
SearchInput,
|
||||
SelectTag,
|
||||
} from '../views/shared/form_components';
|
||||
|
||||
export const renderFilter = (filter, handleChange) => {
|
||||
switch (filter.type) {
|
||||
case 'search':
|
||||
return <SearchInput handleChange={handleChange} input={filter} />;
|
||||
|
||||
case 'select':
|
||||
return <SelectTag handleChange={handleChange} input={filter} />;
|
||||
|
||||
case 'date-range':
|
||||
return <DateRange handleChange={handleChange} input={filter} />;
|
||||
|
||||
case 'input-range':
|
||||
return <InputRangeTag handleChange={handleChange} input={filter} />;
|
||||
|
||||
case 'react-select':
|
||||
return <ReactSelectTag handleChange={handleChange} input={filter} />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const betFilters = [
|
||||
{
|
||||
name: 'outcome',
|
||||
type: 'react-select',
|
||||
placeholder: 'Select...',
|
||||
label: 'Outcome',
|
||||
isMulti: true,
|
||||
options: [
|
||||
{ label: 'All', value: '' },
|
||||
{ label: 'Open', value: 'open' },
|
||||
{ label: 'Lost', value: 'lost' },
|
||||
{ label: 'Won', value: 'won' },
|
||||
{ label: 'Processing', value: 'processing' },
|
||||
{ label: 'Expired', value: 'expired' },
|
||||
{ label: 'Skipped', value: 'skipped' },
|
||||
{ label: 'Ignored', value: 'ignored' },
|
||||
{ label: 'Errored', value: 'errored' },
|
||||
{ label: 'Voided', value: 'voided' },
|
||||
{ label: 'Cancelled', value: 'cancelled' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
type: 'date-range',
|
||||
placeholder: 'Select Date',
|
||||
label: 'Created Date',
|
||||
},
|
||||
{
|
||||
name: 'event_name',
|
||||
type: 'search',
|
||||
placeholder: 'Search...',
|
||||
label: 'Event Name',
|
||||
},
|
||||
];
|
75
portal/client/src/helpers/shared_helpers.js
Normal file
75
portal/client/src/helpers/shared_helpers.js
Normal file
@ -0,0 +1,75 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const toSentence = arr => {
|
||||
if (arr.length > 1)
|
||||
return `${arr.slice(0, arr.length - 1).join(', ')}, and ${arr.slice(-1)}`;
|
||||
|
||||
return arr[0];
|
||||
};
|
||||
|
||||
export const amountFormatter = number => {
|
||||
const unitlist = ['', 'K', 'M', 'G'];
|
||||
let num = number || 0;
|
||||
|
||||
const sign = Math.sign(num);
|
||||
let unit = 0;
|
||||
while (Math.abs(num) > 1000) {
|
||||
unit += 1;
|
||||
num = Math.floor(Math.abs(num) / 100) / 10;
|
||||
}
|
||||
|
||||
return sign * num + unitlist[unit];
|
||||
};
|
||||
|
||||
export const useWindowSize = () => {
|
||||
// Initialize state with undefined width/height so server and client renders match
|
||||
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
});
|
||||
useEffect(() => {
|
||||
// Handler to call on window resize
|
||||
function handleResize() {
|
||||
// Set window width/height to state
|
||||
setWindowSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
// Add event listener
|
||||
window.addEventListener('resize', handleResize);
|
||||
// Call handler right away so state gets updated with initial window size
|
||||
handleResize();
|
||||
// Remove event listener on cleanup
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []); // Empty array ensures that effect is only run on mount
|
||||
return windowSize;
|
||||
};
|
||||
|
||||
export const basicEditorToolbar = [
|
||||
['bold', 'italic', 'underline'], // ['strike'] toggled buttons
|
||||
['blockquote', 'code-block'],
|
||||
['link', 'image'],
|
||||
['emoji'],
|
||||
// [{ header: 1 }, { header: 2 }], // custom button values
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
// [{ script: 'sub' }, { script: 'super' }], // superscript/subscript
|
||||
// [{ indent: '-1' }, { indent: '+1' }], // outdent/indent
|
||||
// [{ direction: 'rtl' }], // text direction
|
||||
|
||||
// [{ size: ['small', false, 'large', 'huge'] }], // custom dropdown
|
||||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||
|
||||
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
|
||||
[{ font: [] }],
|
||||
[{ align: [] }],
|
||||
|
||||
// ['clean'],
|
||||
];
|
||||
|
||||
export const commentEditorToolbar = [
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ font: [] }],
|
||||
['emoji'],
|
||||
];
|
61
portal/client/src/helpers/sidebar_helpers.js
Normal file
61
portal/client/src/helpers/sidebar_helpers.js
Normal file
@ -0,0 +1,61 @@
|
||||
export const primaryLinks = [
|
||||
{
|
||||
linkName: 'dashboard',
|
||||
iconName: 'tachometer-alt',
|
||||
},
|
||||
];
|
||||
|
||||
const controlStyle = {
|
||||
paddingLeft: 15,
|
||||
backgroundColor: '#6D7E8F',
|
||||
color: '#fff',
|
||||
boxShadow: 'none',
|
||||
border: 'none',
|
||||
};
|
||||
|
||||
const commonStyles = {
|
||||
placeholder: provided => ({
|
||||
...provided,
|
||||
color: '#ffffff',
|
||||
}),
|
||||
input: (provided, st) => ({
|
||||
...provided,
|
||||
color: '#fff',
|
||||
}),
|
||||
singleValue: provided => ({
|
||||
...provided,
|
||||
color: '#ffffff',
|
||||
}),
|
||||
option: (provided, state) => ({
|
||||
...provided,
|
||||
backgroundColor: state.isSelected ? '#57B8D7' : '#1f252b',
|
||||
color: '#ffffff',
|
||||
paddingLeft: 20,
|
||||
fontSize: 14,
|
||||
}),
|
||||
menu: provided => ({
|
||||
...provided,
|
||||
backgroundColor: '#1f252b',
|
||||
borderRadius: 15,
|
||||
overflow: 'hidden',
|
||||
padding: 0,
|
||||
}),
|
||||
};
|
||||
|
||||
export const customStylesAlt = {
|
||||
...commonStyles,
|
||||
control: (provided, st) => ({
|
||||
...provided,
|
||||
...controlStyle,
|
||||
borderRadius: '30px 0px 0px 30px',
|
||||
}),
|
||||
};
|
||||
|
||||
export const customStyles = {
|
||||
...commonStyles,
|
||||
control: (provided, st) => ({
|
||||
...provided,
|
||||
...controlStyle,
|
||||
borderRadius: 30,
|
||||
}),
|
||||
};
|
41
portal/client/src/routes/AuthenticatedRoute.jsx
Normal file
41
portal/client/src/routes/AuthenticatedRoute.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Route } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import useStore from '../data/store';
|
||||
|
||||
export const KEYCLOAK_AUTH_URL = '/users/auth/keycloakopenid';
|
||||
|
||||
const AuthenticatedRoute = ({ component, path }) => {
|
||||
const { userStore } = useStore();
|
||||
|
||||
const Component = component;
|
||||
|
||||
const HandleAuth = () => {
|
||||
localStorage.setItem(
|
||||
'redirectURL',
|
||||
`${window.location.pathname}${window.location.search}`
|
||||
);
|
||||
|
||||
window.location.href = KEYCLOAK_AUTH_URL;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Route
|
||||
exact
|
||||
path={path}
|
||||
component={userStore.userSignedIn ? Component : HandleAuth}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
AuthenticatedRoute.propTypes = {
|
||||
component: PropTypes.func,
|
||||
path: PropTypes.string,
|
||||
};
|
||||
|
||||
export default observer(AuthenticatedRoute);
|
36
portal/client/src/routes/index.jsx
Normal file
36
portal/client/src/routes/index.jsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { Route } from 'react-router-dom';
|
||||
import { isEmpty, map } from 'lodash';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import AuthenticatedRoute from './AuthenticatedRoute';
|
||||
|
||||
import * as allRoutes from './routes';
|
||||
import useStore from '../data/store';
|
||||
|
||||
const AppRoutes = () => {
|
||||
const { userStore, appStore } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
userStore.fetchCurrentUser().then(() => {
|
||||
console.log('Fetched user');
|
||||
if (userStore.userSignedIn) appStore.startListeningToAccountChannel();
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (userStore.fetchingCurrentUser) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{map(allRoutes.normalRoutes, (r, i) => (
|
||||
<Route key={i} exact path={r.route} component={r.component} />
|
||||
))}
|
||||
{map(allRoutes.protectedRoutes, (r, i) => (
|
||||
<AuthenticatedRoute key={i} path={r.route} component={r.component} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(AppRoutes);
|
5
portal/client/src/routes/routes.js
Normal file
5
portal/client/src/routes/routes.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Dashboard from '../views/dashboard';
|
||||
import Public from '../views/public';
|
||||
|
||||
export const normalRoutes = [{ route: '/', component: Public }];
|
||||
export const protectedRoutes = [{ route: '/dashboard', component: Dashboard }];
|
44
portal/client/src/t.js
Normal file
44
portal/client/src/t.js
Normal file
@ -0,0 +1,44 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import { isObject, map } from 'lodash';
|
||||
|
||||
import en from '../../config/locales/en.yml';
|
||||
|
||||
const checkForDynamicValues = data => {
|
||||
let updatedData = data;
|
||||
|
||||
if (isObject(data)) {
|
||||
map(Object.keys(updatedData), k => {
|
||||
updatedData[k] = checkForDynamicValues(updatedData[k]);
|
||||
});
|
||||
|
||||
return updatedData;
|
||||
}
|
||||
|
||||
updatedData ||= '';
|
||||
updatedData = updatedData.replaceAll('%', '');
|
||||
updatedData = updatedData.replaceAll('{', '{{');
|
||||
updatedData = updatedData.replaceAll('}', '}}');
|
||||
|
||||
return updatedData;
|
||||
};
|
||||
|
||||
const createLocaleData = (data, key) => ({
|
||||
translation: checkForDynamicValues(data[key]),
|
||||
});
|
||||
|
||||
const resources = {
|
||||
en: createLocaleData(en, 'en'),
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(initReactI18next) // passes i18n down to react-i18next
|
||||
.init({
|
||||
resources,
|
||||
lng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false, // react already safes from xss
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n.t;
|
18
portal/client/src/utils/Notifier.js
Normal file
18
portal/client/src/utils/Notifier.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { store } from 'react-notifications-component';
|
||||
|
||||
const Notifier = (type, title, message) =>
|
||||
store.addNotification({
|
||||
title,
|
||||
message,
|
||||
type,
|
||||
insert: 'top',
|
||||
container: 'top-right',
|
||||
animationIn: ['animate__animated', 'animate__fadeIn'],
|
||||
animationOut: ['animate__animated', 'animate__fadeOut'],
|
||||
dismiss: {
|
||||
duration: 5000,
|
||||
onScreen: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default Notifier;
|
2
portal/client/src/utils/needfulMethods.js
Normal file
2
portal/client/src/utils/needfulMethods.js
Normal file
@ -0,0 +1,2 @@
|
||||
export const getInitials = u => `${u.firstName[0]}${u.lastName[0]}`;
|
||||
export const getFullName = u => `${u.firstName} ${u.lastName}`;
|
84
portal/client/src/views/bets/_bets_list.js
Normal file
84
portal/client/src/views/bets/_bets_list.js
Normal file
@ -0,0 +1,84 @@
|
||||
import { isEmpty, map } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { observer } from 'mobx-react';
|
||||
import ReactTimeAgo from 'react-time-ago';
|
||||
import { Table } from 'reactstrap';
|
||||
import moment from 'moment';
|
||||
|
||||
const BetsList = ({ records }) => {
|
||||
if (isEmpty(records))
|
||||
return <p className="mt-5 font-weight-bold"> No bets </p>;
|
||||
|
||||
return (
|
||||
<Table striped>
|
||||
<thead>
|
||||
<tr className="small">
|
||||
<th>Event</th>
|
||||
<th>Tip Market / Exchange Market Info</th>
|
||||
<th>Tip EV</th>
|
||||
<th>Tip Odds</th>
|
||||
<th>Exchange Odds</th>
|
||||
<th>Executed Odds</th>
|
||||
<th>Stake</th>
|
||||
<th>Projected Profit</th>
|
||||
<th>Outcome</th>
|
||||
<th>Updated</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(records, (record, i) => (
|
||||
<tr key={i}>
|
||||
<td>
|
||||
{record.exchangeEventName}
|
||||
<br />
|
||||
{!isEmpty(record.eventScheduledTime) && (
|
||||
<>
|
||||
<span className="small">
|
||||
{moment(new Date(record.eventScheduledTime)).format(
|
||||
'DD.MMM.YYYY HH:mm:ss'
|
||||
)}
|
||||
</span>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
<span className="small">
|
||||
<span className="text-muted small">
|
||||
{record.exchange}|{record.id}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{' '}
|
||||
{record.marketIdentifier} <br />
|
||||
<span className="text-muted small">
|
||||
{record.exchangeMarketDetails}
|
||||
</span>
|
||||
</td>
|
||||
<td> {record.evPercent} </td>
|
||||
<td> {record.tipProviderOdds} </td>
|
||||
<td style={{ fontSize: 12 }}> {record.exchangeOdds} </td>
|
||||
<td> {record.executedOdds} </td>
|
||||
<td> {record.stake} </td>
|
||||
<td> {record.expectedValue} </td>
|
||||
<td>
|
||||
<span className="btn-link" title={record.log}>
|
||||
{record.outcome}{' '}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<ReactTimeAgo date={new Date(record.createdAt)} />
|
||||
</td>
|
||||
<td> {record.betPlacementType} </td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
BetsList.propTypes = {
|
||||
records: PropTypes.instanceOf(Array).isRequired,
|
||||
};
|
||||
|
||||
export default observer(BetsList);
|
39
portal/client/src/views/bets/index.js
Normal file
39
portal/client/src/views/bets/index.js
Normal file
@ -0,0 +1,39 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import { Card, CardBody } from 'reactstrap';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import FilteredList from '../modules/filtered_list';
|
||||
import useStore from '../../data/store';
|
||||
import { betFilters } from '../../helpers/filter_helpers';
|
||||
import BetsList from './_bets_list';
|
||||
|
||||
const BetsIndex = ({ match }) => {
|
||||
const { betStore } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (betStore.fetched) {
|
||||
console.log('Bets fetched');
|
||||
}
|
||||
}, [betStore.fetched]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<FilteredList
|
||||
listComponent={BetsList}
|
||||
store={betStore}
|
||||
filters={betFilters}
|
||||
recordClassName="col-xl-4"
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
BetsIndex.propTypes = {
|
||||
match: PropTypes.instanceOf(Object).isRequired,
|
||||
};
|
||||
|
||||
export default observer(BetsIndex);
|
33
portal/client/src/views/dashboard/index.js
Normal file
33
portal/client/src/views/dashboard/index.js
Normal file
@ -0,0 +1,33 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { Card, CardBody, Col, Row } from 'reactstrap';
|
||||
import { observer } from 'mobx-react';
|
||||
import ReactTimeAgo from 'react-time-ago';
|
||||
import { map } from 'lodash';
|
||||
import PublicLayout from '../layouts/public_layout';
|
||||
import BetsIndex from '../bets';
|
||||
import useStore from '../../data/store';
|
||||
import CountdownRefresh from '../modules/countdown_refresh';
|
||||
import { SelectTag } from '../shared/form_components';
|
||||
import AuthenticatedLayout from '../layouts/authenticate';
|
||||
|
||||
const Dashboard = () => {
|
||||
const { appStore } = useStore();
|
||||
const renderBody = () => (
|
||||
<>
|
||||
{!appStore.appInitialised && <div>Refreshing...</div>}
|
||||
{appStore.appInitialised && (
|
||||
<div>
|
||||
<Card color="light" className="top-0">
|
||||
<CardBody>
|
||||
<BetsIndex />
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return <AuthenticatedLayout>{renderBody()}</AuthenticatedLayout>;
|
||||
};
|
||||
|
||||
export default observer(Dashboard);
|
96
portal/client/src/views/layouts/authenticate/_header.js
Normal file
96
portal/client/src/views/layouts/authenticate/_header.js
Normal file
@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import {
|
||||
Navbar,
|
||||
Nav,
|
||||
UncontrolledDropdown,
|
||||
DropdownToggle,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
Button,
|
||||
Row,
|
||||
Col,
|
||||
} from 'reactstrap';
|
||||
import { Initial } from 'react-initial';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { isEmpty } from 'lodash';
|
||||
import useStore from '../../../data/store';
|
||||
import { useWindowSize } from '../../../helpers/shared_helpers';
|
||||
|
||||
const _header = () => {
|
||||
const { appStore } = useStore();
|
||||
|
||||
return (
|
||||
<header className="container-lg pt-4">
|
||||
<Row className="sticky-top ">
|
||||
<div>
|
||||
<strong> Results Dashboard</strong>
|
||||
<hr />
|
||||
</div>
|
||||
<Col size={3}>
|
||||
<div className="font-weight-bold">
|
||||
Total Risked: £{appStore.totalRiskedAmount}
|
||||
</div>
|
||||
<div>Total Won: £{appStore.totalWinAmount}</div>
|
||||
<div>Total Lost: £{appStore.totalLostAmount}</div>
|
||||
<div>
|
||||
<strong>
|
||||
Total Profit:{' '}
|
||||
<span
|
||||
className={
|
||||
appStore.totalWinAmount - appStore.totalLostAmount >= 0
|
||||
? 'text-success'
|
||||
: 'text-danger'
|
||||
}
|
||||
>
|
||||
£
|
||||
{Math.round(appStore.totalWinAmount - appStore.totalLostAmount)}
|
||||
</span>
|
||||
</strong>
|
||||
</div>
|
||||
</Col>
|
||||
<Col size={3}>
|
||||
<div>Total Tips: {appStore.totalTips}</div>
|
||||
<div>Total Tips Executed: {appStore.totalPlacedBets}</div>
|
||||
<div>Total Tips Skipped: {appStore.totalTipsSkipped}</div>
|
||||
</Col>
|
||||
<Col size={3}>
|
||||
<div className="fw-bold">
|
||||
Total Bets Placed: {appStore.totalPlacedBets}
|
||||
</div>
|
||||
<div>Total Tips Expired: {appStore.totalTipsExpired}</div>
|
||||
<div>Total Tips Ignored: {appStore.totalTipsIgnored}</div>
|
||||
<div>Total Tips Voided: {appStore.totalTipsVoided}</div>
|
||||
</Col>
|
||||
<Col size={3}>
|
||||
<div>Total Bets Open: {appStore.totalPlacedBetsOpen}</div>
|
||||
<div>
|
||||
Total Bets Won: {appStore.totalPlacedBetsWon}
|
||||
<span className="small text-info">
|
||||
{' '}
|
||||
({appStore.asPercentOfTotal(appStore.totalPlacedBetsWon)})
|
||||
</span>
|
||||
</div>
|
||||
<div className="mx-3 text-muted small">
|
||||
Average Odds Won at: {appStore.averageOddsWon}
|
||||
</div>
|
||||
<div>
|
||||
Total Bets Lost: {appStore.totalPlacedBetsLost}
|
||||
<span className="small text-info">
|
||||
{' '}
|
||||
({appStore.asPercentOfTotal(appStore.totalPlacedBetsLost)})
|
||||
</span>
|
||||
</div>
|
||||
<div className="mx-3 text-muted small">
|
||||
Average Odds Lost at: {appStore.averageOddsLost}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr />
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(_header);
|
151
portal/client/src/views/layouts/authenticate/_sidebar.js
Normal file
151
portal/client/src/views/layouts/authenticate/_sidebar.js
Normal file
@ -0,0 +1,151 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import { observer } from 'mobx-react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import ReactTimeAgo from 'react-time-ago';
|
||||
import { map } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { Form, FormGroup, Input, Label } from 'reactstrap';
|
||||
import LOGO from '../../../assets/images/logo.svg';
|
||||
import useStore from '../../../data/store';
|
||||
import { SelectTag } from '../../shared/form_components';
|
||||
import CountdownRefresh from '../../modules/countdown_refresh';
|
||||
import EditableHash from '../../modules/editable_hash';
|
||||
import Toggler from '../../modules/toggler.js';
|
||||
|
||||
const _sidebar = () => {
|
||||
const { appStore } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
appStore.refresh();
|
||||
}, []);
|
||||
|
||||
const changeExchangeAccount = e => {
|
||||
appStore.update({ currentExchangeAccountId: e.target.value });
|
||||
appStore.refresh();
|
||||
};
|
||||
|
||||
const exchangeAccountsAsOptions = () => ({
|
||||
name: 'exchangeAccounts',
|
||||
value: appStore.currentExchangeAccountId,
|
||||
type: 'select',
|
||||
placeholder: 'Select...',
|
||||
label: 'Available Accounts',
|
||||
options: map(appStore.exchangeAccounts, r => ({ label: r, value: r })),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{!appStore.appInitialised && <div>Refreshing...</div>}
|
||||
{appStore.appInitialised && (
|
||||
<div className="sidebar bg-light">
|
||||
<div className="sidebar-width">
|
||||
<div className="logo text-center">
|
||||
<Link to="/dashboard">
|
||||
<img src={LOGO} alt="logo" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="separator" />
|
||||
<div className="sidebar-links">
|
||||
<div className="p-0 small">
|
||||
<div className="text-primary mb-3">
|
||||
<div className="mb-1">Available Accounts:</div>
|
||||
<SelectTag
|
||||
handleChange={changeExchangeAccount}
|
||||
input={exchangeAccountsAsOptions()}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Toggler
|
||||
prompt
|
||||
state={appStore.exchangeAccount.isActive}
|
||||
onText="Activate account"
|
||||
offText="Deactivate account"
|
||||
onChangeHandler={() => {
|
||||
appStore.exchangeAccount.updateExchangeAccount({
|
||||
isActive: !appStore.exchangeAccount.isActive,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Toggler
|
||||
prompt
|
||||
disabled={!appStore.exchangeAccount.isActive}
|
||||
state={appStore.exchangeAccount.canBet}
|
||||
onText="Enable live betting"
|
||||
offText="Disable live betting"
|
||||
onChangeHandler={() => {
|
||||
appStore.exchangeAccount.updateExchangeAccount({
|
||||
canBet: !appStore.exchangeAccount.canBet,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<CountdownRefresh
|
||||
refreshTrigger={appStore.autoRefreshResults}
|
||||
refreshFunction={appStore.refresh}
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="small">
|
||||
<div className="font-weight-bold text-primary mb-1">
|
||||
Account Balance: £{appStore.exchangeAccount.accountBalance}
|
||||
</div>
|
||||
<div className="text-primary">
|
||||
Running since:{' '}
|
||||
<ReactTimeAgo date={new Date(appStore.runningSince)} />
|
||||
</div>
|
||||
<div className="text-primary">
|
||||
Feed type: {appStore.exchangeAccount.sourceTypes}
|
||||
</div>
|
||||
<div className="text-primary">
|
||||
Stake Strategy: {appStore.exchangeAccount.stakeStrategyName}
|
||||
</div>
|
||||
<div className="text-primary mb-1">
|
||||
Strategy Config:
|
||||
<EditableHash
|
||||
source={appStore.exchangeAccount.stakeStrategyConfig}
|
||||
updateFunction={() => {
|
||||
alert('this');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<div className="small text-secondary">
|
||||
<strong>
|
||||
Last log was at{' '}
|
||||
{moment(
|
||||
new Date(appStore.exchangeAccount.lastLogTime)
|
||||
).format('DD.MMM.YYYY HH:mm:ss')}
|
||||
</strong>
|
||||
</div>
|
||||
<code className="small text-muted mb-1">
|
||||
{appStore.exchangeAccount.log}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="separator" />
|
||||
<div className="sidebar-links">
|
||||
<ul>
|
||||
<li className="d-flex">
|
||||
<a href="/admin/sidekiq" target="sidekiq">
|
||||
<FontAwesomeIcon icon="cogs" /> Sidekiq
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
;
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(_sidebar);
|
44
portal/client/src/views/layouts/authenticate/index.js
Normal file
44
portal/client/src/views/layouts/authenticate/index.js
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from 'reactstrap';
|
||||
import classnames from 'classnames';
|
||||
import Header from './_header';
|
||||
import Sidebar from './_sidebar';
|
||||
import useStore from '../../../data/store';
|
||||
|
||||
const AuthenticatedLayout = ({ children, loading }) => {
|
||||
const { appStore } = useStore();
|
||||
|
||||
return (
|
||||
<div className="authenticated-layout">
|
||||
<div
|
||||
className={classnames('bootstrap-layout', {
|
||||
'sidebar-opened': appStore.sidebarOpened,
|
||||
'sidebar-closed': !appStore.sidebarOpened,
|
||||
})}
|
||||
>
|
||||
<div className="bl-sidebar">
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="bl-content">
|
||||
<Header />
|
||||
<main>{loading ? 'Loading' : children}</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AuthenticatedLayout.defaultProps = {
|
||||
loading: false,
|
||||
};
|
||||
|
||||
AuthenticatedLayout.propTypes = {
|
||||
children: PropTypes.node,
|
||||
loading: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default observer(AuthenticatedLayout);
|
40
portal/client/src/views/layouts/public_layout.js
Normal file
40
portal/client/src/views/layouts/public_layout.js
Normal file
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Container, Navbar, NavbarBrand } from 'reactstrap';
|
||||
import LOGO from '../../assets/images/logo.svg';
|
||||
import useStore from '../../data/store';
|
||||
|
||||
const PublicLayout = ({ children }) => {
|
||||
const { userStore } = useStore();
|
||||
return (
|
||||
<div
|
||||
className="bg-light pt-5"
|
||||
style={{ height: '100vh', overflowY: 'auto' }}
|
||||
>
|
||||
<Navbar fixed="top" light className="bg-light justify-content-lg-center">
|
||||
<NavbarBrand>
|
||||
<img src={LOGO} alt="logo" />
|
||||
</NavbarBrand>
|
||||
{userStore.userSignedIn && (
|
||||
<div>
|
||||
<a href="/admin/sidekiq" target="sidekiq">
|
||||
Sidekiq
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</Navbar>
|
||||
<Container className="mt-5 container-xl border-top border-dark">
|
||||
{children}
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PublicLayout.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default observer(PublicLayout);
|
54
portal/client/src/views/modules/countdown_refresh/index.js
Normal file
54
portal/client/src/views/modules/countdown_refresh/index.js
Normal file
@ -0,0 +1,54 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import { Form, FormGroup, Input, Label } from 'reactstrap';
|
||||
import useStore from '../../../data/store';
|
||||
import Toggler from '../toggler.js';
|
||||
|
||||
const CountdownRefresh = () => {
|
||||
const { appStore } = useStore();
|
||||
const refreshTime = 1;
|
||||
const [countdownToNextRefresh, setCountdownToNextRefresh] = useState(
|
||||
refreshTime
|
||||
);
|
||||
const [enableCountdown, setEnableCountdown] = useState(
|
||||
appStore.autoRefreshResults
|
||||
);
|
||||
const [intervalId, setIntervalId] = useState();
|
||||
let counter = refreshTime;
|
||||
const startCountdown = () => {
|
||||
setIntervalId(
|
||||
setInterval(() => {
|
||||
counter -= 1;
|
||||
if (counter === 0) {
|
||||
counter = refreshTime;
|
||||
appStore.refresh();
|
||||
}
|
||||
setCountdownToNextRefresh(counter);
|
||||
}, 1000)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (enableCountdown) {
|
||||
startCountdown();
|
||||
}
|
||||
}, [enableCountdown]);
|
||||
|
||||
const handleChange = () => {
|
||||
setEnableCountdown(!enableCountdown);
|
||||
console.log('Stopping counter refresh');
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
|
||||
return (
|
||||
<Toggler
|
||||
state={enableCountdown}
|
||||
offText={'Disable auto-refresh'}
|
||||
onText="Enable auto-refresh"
|
||||
onChangeHandler={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(CountdownRefresh);
|
67
portal/client/src/views/modules/editable_hash/index.js
Normal file
67
portal/client/src/views/modules/editable_hash/index.js
Normal file
@ -0,0 +1,67 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { forEach, includes, isEmpty, isNull } from 'lodash';
|
||||
import { Button } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const EditableHash = ({ source, updateFunction }) => {
|
||||
const editableValues = [];
|
||||
//
|
||||
// const editableValues = [
|
||||
// 'max_ev',
|
||||
// 'min_ev',
|
||||
// 'odds_margin',
|
||||
// 'max_odds_to_bet',
|
||||
// 'min_odds_to_bet',
|
||||
// 'stake_sizing',
|
||||
// 'max_bankroll_per_bet',
|
||||
// ];
|
||||
|
||||
const showOnlyEditable = false;
|
||||
const divOfValues = hash => {
|
||||
if (Object.keys(hash).length === 0 || typeof hash === 'string') {
|
||||
return hash;
|
||||
}
|
||||
const x = [];
|
||||
// forEach(Object.keys(hash), key => {
|
||||
// if (includes(editableValues, key)){
|
||||
// const v = divOfValues(hash[key])
|
||||
// const vEntry = <input id={key} value={v}/>
|
||||
// x.push(
|
||||
// <div>{key}: {vEntry}</div>
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
|
||||
forEach(Object.keys(hash), key => {
|
||||
const v = divOfValues(hash[key]);
|
||||
const vEntry = includes(editableValues, key) ? (
|
||||
<input id={key} value={v} />
|
||||
) : (
|
||||
v
|
||||
);
|
||||
const hideThis = showOnlyEditable && !includes(editableValues, key)
|
||||
|
||||
if (!hideThis) {
|
||||
x.push(
|
||||
<div>
|
||||
{key}: {vEntry}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
return <div className="small px-2 border-start">{x}</div>;
|
||||
};
|
||||
return (
|
||||
<div className="mt-2 small px-2 text-secondary">{divOfValues(source)}</div>
|
||||
);
|
||||
};
|
||||
|
||||
EditableHash.propTypes = {
|
||||
source: PropTypes.instanceOf(Object).isRequired,
|
||||
updateFunction: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default observer(EditableHash);
|
55
portal/client/src/views/modules/filtered_list/_filters.js
Normal file
55
portal/client/src/views/modules/filtered_list/_filters.js
Normal file
@ -0,0 +1,55 @@
|
||||
import React, { Fragment, useState } from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { isBoolean, map } from 'lodash';
|
||||
import { Col, FormGroup, Label, Row } from 'reactstrap';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { renderFilter } from '../../../helpers/filter_helpers';
|
||||
|
||||
const Filters = ({ pagination, filters }) => {
|
||||
const [timeout, setTimeOut] = useState(null);
|
||||
|
||||
const handleChange = e => {
|
||||
let timer = null;
|
||||
|
||||
setTimeOut(clearTimeout(timeout));
|
||||
|
||||
pagination.update({
|
||||
params: { ...pagination.params, [e.target.name]: e.target.value },
|
||||
});
|
||||
|
||||
timer = setTimeout(() => {
|
||||
pagination.resetPageAndFetch();
|
||||
}, 1000);
|
||||
|
||||
setTimeOut(timer);
|
||||
};
|
||||
|
||||
return (
|
||||
<Row className="">
|
||||
{map(filters, (filter, i) => (
|
||||
<Fragment>
|
||||
{isBoolean(filter.show) && !filter.show ? null : (
|
||||
<Col key={i}>
|
||||
<FormGroup>
|
||||
<Label>{filter.label}</Label>
|
||||
{renderFilter(
|
||||
{ ...filter, value: pagination.params[filter.name] },
|
||||
handleChange
|
||||
)}
|
||||
</FormGroup>
|
||||
</Col>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
Filters.propTypes = {
|
||||
pagination: PropTypes.instanceOf(Object).isRequired,
|
||||
filters: PropTypes.instanceOf(Array).isRequired,
|
||||
};
|
||||
|
||||
export default observer(Filters);
|
111
portal/client/src/views/modules/filtered_list/index.js
Normal file
111
portal/client/src/views/modules/filtered_list/index.js
Normal file
@ -0,0 +1,111 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { isEmpty, isNull } from 'lodash';
|
||||
import { Button } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import Filters from './_filters';
|
||||
|
||||
const FilteredList = ({
|
||||
store,
|
||||
listComponent,
|
||||
gridComponent,
|
||||
filters,
|
||||
changeTrigger,
|
||||
listClassName,
|
||||
recordClassName,
|
||||
}) => {
|
||||
const ListComponent = listComponent;
|
||||
// const Component = gridComponent;
|
||||
|
||||
useEffect(() => {
|
||||
store.pagination.fetch();
|
||||
}, [changeTrigger]);
|
||||
|
||||
return (
|
||||
<div className="filtered-lists">
|
||||
<Filters filters={filters} pagination={store.pagination} />
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul className="pagination justify-content-start">
|
||||
<li className="small text-muted mt-2" style={{marginRight: 10}}>
|
||||
<strong>
|
||||
{store.pagination.from} to {store.pagination.to} of{' '}
|
||||
{store.pagination.count}{' '}
|
||||
</strong>
|
||||
</li>
|
||||
<li className="">
|
||||
<Button
|
||||
className="btn-link page-link"
|
||||
onClick={() => store.pagination.gotoPage(1)}
|
||||
disabled={isNull(store.pagination.prev)}
|
||||
>
|
||||
First
|
||||
</Button>
|
||||
</li>
|
||||
<li className="page-item">
|
||||
<Button
|
||||
className="btn-link page-link"
|
||||
disabled={isNull(store.pagination.prev)}
|
||||
onClick={() => store.pagination.gotoPage(store.pagination.prev)}
|
||||
>
|
||||
Prev
|
||||
</Button>
|
||||
</li>
|
||||
<li className="page-item">
|
||||
<Button
|
||||
className="btn-link page-link"
|
||||
disabled={isNull(store.pagination.next)}
|
||||
onClick={() => store.pagination.gotoPage(store.pagination.next)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</li>
|
||||
<li className="page-item">
|
||||
<Button
|
||||
className="btn-link page-link"
|
||||
disabled={
|
||||
isNull(store.pagination.last) || store.pagination.count === 0
|
||||
}
|
||||
onClick={() => store.pagination.gotoPage(store.pagination.last)}
|
||||
>
|
||||
Last
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{isEmpty(store.filteredRecords) && (
|
||||
<>
|
||||
<hr />
|
||||
<strong className="text-muted">Nothing here.</strong>
|
||||
</>
|
||||
)}
|
||||
{!isEmpty(store.filteredRecords) && (
|
||||
<div className="records mt-4">
|
||||
<div className={listClassName}>
|
||||
<div className="table-responsive">
|
||||
<ListComponent records={store.filteredRecords} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FilteredList.defaultProps = {
|
||||
listClassName: '',
|
||||
recordClassName: '',
|
||||
};
|
||||
|
||||
FilteredList.propTypes = {
|
||||
store: PropTypes.instanceOf(Object).isRequired,
|
||||
listComponent: PropTypes.func.isRequired,
|
||||
gridComponent: PropTypes.func,
|
||||
filters: PropTypes.instanceOf(Array).isRequired,
|
||||
changeTrigger: PropTypes.string.isRequired,
|
||||
listClassName: PropTypes.string,
|
||||
recordClassName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default observer(FilteredList);
|
58
portal/client/src/views/modules/toggler.js/index.js
Normal file
58
portal/client/src/views/modules/toggler.js/index.js
Normal file
@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import { Form, FormGroup, Input, Label } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Toggler = ({
|
||||
prompt,
|
||||
disabled,
|
||||
state,
|
||||
onText,
|
||||
offText,
|
||||
onChangeHandler,
|
||||
}) => {
|
||||
const handleChange = () => {
|
||||
if (prompt) {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
if (confirm('Are you sure?') === false) return false;
|
||||
}
|
||||
onChangeHandler();
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<FormGroup switch disabled={disabled}>
|
||||
<Label
|
||||
style={{ fontSize: 13 }}
|
||||
className={state ? 'text-primary' : 'text-danger'}
|
||||
check
|
||||
>
|
||||
{state ? offText : onText}
|
||||
</Label>
|
||||
<Input
|
||||
disabled={disabled}
|
||||
type="switch"
|
||||
role="switch"
|
||||
checked={state}
|
||||
onClick={handleChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
Toggler.defaultProps = {
|
||||
disabled: false,
|
||||
prompt: false,
|
||||
};
|
||||
|
||||
Toggler.propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
prompt: PropTypes.bool,
|
||||
state: PropTypes.bool.isRequired,
|
||||
onText: PropTypes.string.isRequired,
|
||||
offText: PropTypes.string.isRequired,
|
||||
onChangeHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
export default observer(Toggler);
|
47
portal/client/src/views/public/index.js
Normal file
47
portal/client/src/views/public/index.js
Normal file
@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from 'reactstrap';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import useStore from '../../data/store';
|
||||
import { KEYCLOAK_AUTH_URL } from '../../routes/AuthenticatedRoute';
|
||||
import PublicLayout from '../layouts/public_layout';
|
||||
|
||||
const Public = () => {
|
||||
const { userStore } = useStore();
|
||||
|
||||
const handleSignInSignUp = () => {
|
||||
localStorage.setItem(
|
||||
'redirectURL',
|
||||
`${window.location.pathname}${window.location.search}`
|
||||
);
|
||||
|
||||
window.location.href = KEYCLOAK_AUTH_URL;
|
||||
};
|
||||
|
||||
const handleSignOut = () => {
|
||||
window.location.href = '/sign_out';
|
||||
};
|
||||
|
||||
const renderBody = () => (
|
||||
<div>
|
||||
<div className="mt-3 w-100 text-center">Welcome to BetBeast.</div>
|
||||
<div className="mt-5 text-center">
|
||||
{userStore.userSignedIn && (
|
||||
<div>
|
||||
<Link to="/dashboard" className="btn btn-primary mr-2">
|
||||
Go to Dashboard
|
||||
</Link>
|
||||
<Button onClick={handleSignOut}> Sign Out</Button>
|
||||
</div>
|
||||
)}
|
||||
{!userStore.userSignedIn && (
|
||||
<Button onClick={handleSignInSignUp}> Sign In</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <PublicLayout>{renderBody()}</PublicLayout>;
|
||||
};
|
||||
|
||||
export default observer(Public);
|
182
portal/client/src/views/shared/form_components.js
Normal file
182
portal/client/src/views/shared/form_components.js
Normal file
@ -0,0 +1,182 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button, Input, Popover, PopoverBody } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { find, filter, isDate, isObject, map, includes } from 'lodash';
|
||||
import { DateRangePicker } from 'react-date-range';
|
||||
import InputRange from 'react-input-range';
|
||||
import Select from 'react-select';
|
||||
import moment from 'moment';
|
||||
|
||||
export const SearchInput = ({ input, handleChange }) => (
|
||||
<Input
|
||||
placeholder={input.placeholder}
|
||||
name={input.name}
|
||||
onChange={handleChange}
|
||||
value={input.value}
|
||||
type="search"
|
||||
/>
|
||||
);
|
||||
|
||||
export const SelectTag = ({ input, handleChange }) => (
|
||||
<Input
|
||||
name={input.name}
|
||||
id={input.name}
|
||||
onChange={handleChange}
|
||||
value={input.value}
|
||||
type="select"
|
||||
>
|
||||
{map(input.options, (opt, i) => (
|
||||
<option key={i} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</Input>
|
||||
);
|
||||
|
||||
export const DateRange = ({ input, handleChange }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const togglePopover = () => setIsOpen(!isOpen);
|
||||
|
||||
const dateLabel = () => {
|
||||
if (!isDate(input.value.from) && !isDate(input.value.to))
|
||||
return input.placeholder;
|
||||
|
||||
const startDate = isDate(input.value.from)
|
||||
? moment(input.value.from).format('MMM/DD/YYYY')
|
||||
: 'NA';
|
||||
|
||||
const endDate = isDate(input.value.to)
|
||||
? moment(input.value.to).format('MMM/DD/YYYY')
|
||||
: 'NA';
|
||||
|
||||
return `${startDate} - ${endDate}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dt-range-pick">
|
||||
<Input
|
||||
value={dateLabel()}
|
||||
disabled
|
||||
id="date-range-picker-popover"
|
||||
onClick={togglePopover}
|
||||
/>
|
||||
{isDate(input.value.from) && (
|
||||
<Button
|
||||
color="link"
|
||||
onClick={() =>
|
||||
handleChange({
|
||||
target: {
|
||||
name: input.name,
|
||||
value: {
|
||||
from: null,
|
||||
to: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
)}
|
||||
<Popover
|
||||
className="no-max-width"
|
||||
toggle={togglePopover}
|
||||
isOpen={isOpen}
|
||||
target="date-range-picker-popover"
|
||||
trigger="legacy"
|
||||
placement="bottom"
|
||||
>
|
||||
<PopoverBody>
|
||||
<DateRangePicker
|
||||
ranges={[
|
||||
{
|
||||
startDate: input.value.from || new Date(),
|
||||
endDate: input.value.to || new Date(),
|
||||
key: input.name,
|
||||
},
|
||||
]}
|
||||
onChange={d =>
|
||||
handleChange({
|
||||
target: {
|
||||
name: input.name,
|
||||
value: {
|
||||
from: d[input.name].startDate,
|
||||
to: d[input.name].endDate,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</PopoverBody>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InputRangeTag = ({ input, handleChange }) => (
|
||||
<InputRange
|
||||
maxValue={input.max || 1000}
|
||||
minValue={input.min || 0}
|
||||
step={input.step || 1}
|
||||
value={
|
||||
isObject(input.value)
|
||||
? { min: input.value.min, max: input.value.max }
|
||||
: input.value
|
||||
}
|
||||
onChange={value =>
|
||||
handleChange({
|
||||
target: {
|
||||
name: input.name,
|
||||
value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ReactSelectTag = ({ input, handleChange }) => (
|
||||
<Select
|
||||
options={input.options}
|
||||
value={
|
||||
input.isMulti
|
||||
? filter(input.options, o => includes(input.value, o.value))
|
||||
: find(input.options, { value: input.value })
|
||||
}
|
||||
isMulti={input.isMulti}
|
||||
onChange={e => {
|
||||
handleChange({
|
||||
target: {
|
||||
name: input.name,
|
||||
value: input.isMulti ? map(e, v => v.value) : e.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
SelectTag.propTypes = {
|
||||
input: PropTypes.instanceOf(Object).isRequired,
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
SearchInput.propTypes = {
|
||||
input: PropTypes.instanceOf(Object).isRequired,
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
DateRange.propTypes = {
|
||||
input: PropTypes.instanceOf(Object).isRequired,
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
InputRangeTag.propTypes = {
|
||||
input: PropTypes.instanceOf(Object).isRequired,
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ReactSelectTag.propTypes = {
|
||||
input: PropTypes.instanceOf(Object).isRequired,
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
};
|
29
portal/client/src/views/shared/info_point.js
Normal file
29
portal/client/src/views/shared/info_point.js
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { observer } from 'mobx-react';
|
||||
import {
|
||||
Button,
|
||||
PopoverBody,
|
||||
PopoverHeader,
|
||||
UncontrolledPopover,
|
||||
} from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
const InfoPoint = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
icon="info-circle"
|
||||
className="text-info pl-1 pt-1"
|
||||
/>{' '}
|
||||
<UncontrolledPopover trigger="focus">
|
||||
<PopoverBody>{children}</PopoverBody>
|
||||
</UncontrolledPopover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
InfoPoint.propTypes = {
|
||||
children: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default observer(InfoPoint);
|
Reference in New Issue
Block a user