├── client ├── src │ ├── include │ │ ├── popper.js │ │ ├── jquery.js │ │ └── bootstrap.js │ ├── modules │ │ ├── index.js │ │ ├── keys.js │ │ ├── auth.js │ │ ├── error.js │ │ ├── cases.js │ │ └── unbox.js │ ├── containers │ │ ├── cases │ │ │ ├── shuffle.js │ │ │ ├── remaining-keys.js │ │ │ ├── index.js │ │ │ ├── case.js │ │ │ └── autopick.js │ │ ├── login │ │ │ ├── callback.js │ │ │ ├── button.js │ │ │ └── index.js │ │ ├── header │ │ │ ├── fixed.js │ │ │ ├── error-listener.js │ │ │ ├── error.js │ │ │ ├── nav-user.js │ │ │ └── floating.js │ │ ├── unbox │ │ │ ├── poller.js │ │ │ ├── item.js │ │ │ ├── index.js │ │ │ ├── confirmation.js │ │ │ ├── waiting.js │ │ │ └── result.js │ │ ├── home │ │ │ └── index.js │ │ ├── app │ │ │ └── index.js │ │ ├── developers │ │ │ └── index.js │ │ ├── info │ │ │ └── index.js │ │ └── faq │ │ │ └── index.js │ ├── index.js │ ├── store.js │ ├── actions.js │ └── styles.css ├── public │ ├── favicon.ico │ ├── img │ │ ├── back-QA.png │ │ ├── divisor.jpg │ │ ├── divisor.png │ │ ├── line-01.png │ │ ├── back-footer.jpg │ │ ├── back-header.png │ │ ├── back-slide.jpg │ │ ├── logo-footer.png │ │ ├── logo-header.png │ │ ├── default-vcase.png │ │ ├── degrade-slide.png │ │ ├── steam-signin-button.png │ │ └── spinner.svg │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── mstile-150x150.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── browserconfig.xml │ ├── site.webmanifest │ ├── index.html │ └── safari-pinned-tab.svg ├── .gitignore └── package.json ├── vgomock ├── public │ ├── img │ │ ├── img-01.png │ │ ├── img-02.png │ │ └── img-03.png │ └── trade_offer.html ├── .gitignore ├── package.json ├── index.js └── package-lock.json ├── start-client.js ├── start-vgo-mock.js ├── log.js ├── .gitignore ├── Makefile ├── LICENSE ├── package.json ├── README.md ├── config.js └── server.js /client/src/include/popper.js: -------------------------------------------------------------------------------- 1 | import Popper from 'popper.js'; 2 | 3 | window.Popper = Popper; 4 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/src/include/jquery.js: -------------------------------------------------------------------------------- 1 | import * as $ from 'jquery'; 2 | 3 | window.jQuery = window.$ = $; 4 | -------------------------------------------------------------------------------- /client/public/img/back-QA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/img/back-QA.png -------------------------------------------------------------------------------- /client/public/img/divisor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/img/divisor.jpg -------------------------------------------------------------------------------- /client/public/img/divisor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/img/divisor.png -------------------------------------------------------------------------------- /client/public/img/line-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/img/line-01.png -------------------------------------------------------------------------------- /vgomock/public/img/img-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/vgomock/public/img/img-01.png -------------------------------------------------------------------------------- /vgomock/public/img/img-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/vgomock/public/img/img-02.png -------------------------------------------------------------------------------- /vgomock/public/img/img-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/vgomock/public/img/img-03.png -------------------------------------------------------------------------------- /client/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/favicon-16x16.png -------------------------------------------------------------------------------- /client/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/favicon-32x32.png -------------------------------------------------------------------------------- /client/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/apple-touch-icon.png -------------------------------------------------------------------------------- /client/public/img/back-footer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/img/back-footer.jpg -------------------------------------------------------------------------------- /client/public/img/back-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/img/back-header.png -------------------------------------------------------------------------------- /client/public/img/back-slide.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/img/back-slide.jpg -------------------------------------------------------------------------------- /client/public/img/logo-footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/img/logo-footer.png -------------------------------------------------------------------------------- /client/public/img/logo-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/img/logo-header.png -------------------------------------------------------------------------------- /client/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/mstile-150x150.png -------------------------------------------------------------------------------- /client/public/img/default-vcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/img/default-vcase.png -------------------------------------------------------------------------------- /client/public/img/degrade-slide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/img/degrade-slide.png -------------------------------------------------------------------------------- /client/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /client/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /client/public/img/steam-signin-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoskins/vcase/HEAD/client/public/img/steam-signin-button.png -------------------------------------------------------------------------------- /client/src/include/bootstrap.js: -------------------------------------------------------------------------------- 1 | import './jquery'; 2 | import './popper'; 3 | 4 | import 'bootstrap'; 5 | import 'bootstrap/dist/css/bootstrap.min.css'; 6 | -------------------------------------------------------------------------------- /start-client.js: -------------------------------------------------------------------------------- 1 | const args = ['start']; 2 | const opts = { stdio: 'inherit', cwd: 'client', shell: true }; 3 | require('child_process').spawn('npm', args, opts); 4 | -------------------------------------------------------------------------------- /start-vgo-mock.js: -------------------------------------------------------------------------------- 1 | const args = ['start']; 2 | const opts = { stdio: 'inherit', cwd: 'vgomock', shell: true }; 3 | require('child_process').spawn('npm', args, opts); 4 | -------------------------------------------------------------------------------- /vgomock/public/trade_offer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Trade Offer 6 | 7 | 8 | 9 |

10 | Trade Offer Details! 11 |

12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /log.js: -------------------------------------------------------------------------------- 1 | const bunyan = require('bunyan'); 2 | const config = require('./config'); 3 | 4 | const logConfig = Object.assign( 5 | { 6 | name: 'VCase', 7 | level: 'info', 8 | }, 9 | config.log 10 | ); 11 | 12 | module.exports = bunyan.createLogger(logConfig); 13 | -------------------------------------------------------------------------------- /client/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /vgomock/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /public 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /vgomock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vgomock", 3 | "version": "1.0.0", 4 | "description": "Mock of the VGO API", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "dependencies": { 10 | "koa": "^2.5.1", 11 | "koa-bunyan-logger": "^2.0.0", 12 | "koa-qs": "^2.0.0", 13 | "koa-router": "^7.4.0", 14 | "koa-static": "^4.0.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/src/modules/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer } from 'react-router-redux'; 3 | import cases from './cases'; 4 | import unbox from './unbox'; 5 | import keys from './keys'; 6 | import error from './error'; 7 | import auth from './auth'; 8 | 9 | export default combineReducers({ 10 | routing: routerReducer, 11 | cases, 12 | unbox, 13 | auth, 14 | keys, 15 | error, 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/modules/keys.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_STATE = { available: 0, minimum: 3, loaded: false }; 2 | const keys = (state = DEFAULT_STATE, action) => { 3 | switch (action.type) { 4 | case 'GET_AVAILABLE_KEYS_RECEIVED': 5 | return Object.assign({}, state, { 6 | available: action.keyCount, 7 | loaded: true, 8 | }); 9 | case 'EXIT': 10 | case 'AUTH_STATUS': 11 | return DEFAULT_STATE; 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | export default keys; 18 | -------------------------------------------------------------------------------- /client/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /client/src/containers/cases/shuffle.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { shuffleCases } from '../../actions'; 4 | 5 | class ShuffleButton extends Component { 6 | shuffle() { 7 | this.props.dispatch(shuffleCases()); 8 | } 9 | 10 | render() { 11 | return ( 12 | 15 | ); 16 | } 17 | } 18 | 19 | export default connect()(ShuffleButton); 20 | -------------------------------------------------------------------------------- /client/src/modules/auth.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_STATE = { 2 | authenticated: false, 3 | avatar: '', 4 | username: '', 5 | }; 6 | const auth = (state = DEFAULT_STATE, action) => { 7 | switch (action.type) { 8 | case 'AUTH_STATUS': 9 | return Object.assign({}, state, { 10 | authenticated: action.authenticated, 11 | avatar: action.avatar, 12 | username: action.username, 13 | }); 14 | case 'EXIT': 15 | return DEFAULT_STATE; 16 | default: 17 | return state; 18 | } 19 | }; 20 | 21 | export default auth; 22 | -------------------------------------------------------------------------------- /client/src/containers/login/callback.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { getAuthStatus } from '../../actions'; 4 | 5 | class LoginCallback extends Component { 6 | componentWillMount() { 7 | this.props.dispatch(getAuthStatus()); 8 | } 9 | 10 | render() { 11 | if (this.props.authenticated) { 12 | window.close(); 13 | } 14 | return
; 15 | } 16 | } 17 | 18 | export default connect(state => ({ 19 | authenticated: state.auth.authenticated, 20 | }))(LoginCallback); 21 | -------------------------------------------------------------------------------- /client/src/containers/header/fixed.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ErrorListener from './error-listener'; 4 | import NavUser from './nav-user'; 5 | 6 | class FixedHeader extends Component { 7 | render() { 8 | return ( 9 | 18 | ); 19 | } 20 | } 21 | 22 | export default connect()(FixedHeader); 23 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { ConnectedRouter } from 'react-router-redux'; 5 | import store, { history } from './store'; 6 | import App from './containers/app'; 7 | 8 | import './include/bootstrap'; 9 | 10 | import 'font-awesome/css/font-awesome.css'; 11 | import 'simple-line-icons/css/simple-line-icons.css'; 12 | import './styles.css'; 13 | 14 | const target = document.querySelector('#root'); 15 | 16 | render( 17 | 18 | 19 | 20 | 21 | , 22 | target 23 | ); 24 | -------------------------------------------------------------------------------- /client/src/containers/login/button.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | class LoginButton extends Component { 5 | openLoginPopup(event) { 6 | event.preventDefault(); 7 | window.open( 8 | '/auth', 9 | 'login', 10 | 'height=800,width=1028,resize=yes,scrollbars=yes' 11 | ); 12 | } 13 | 14 | render() { 15 | return ( 16 | 21 | 22 | 23 | ); 24 | } 25 | } 26 | 27 | export default connect()(LoginButton); 28 | -------------------------------------------------------------------------------- /client/src/containers/unbox/poller.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { getUnboxStatus } from '../../actions'; 4 | 5 | const POLL_INTERVAL_MS = 10000; 6 | 7 | class UnboxPoller extends Component { 8 | componentDidMount() { 9 | this.interval = setInterval(() => { 10 | this.pollUnboxStatus(); 11 | }, POLL_INTERVAL_MS); 12 | } 13 | 14 | componentWillUnmount() { 15 | clearInterval(this.interval); 16 | } 17 | 18 | pollUnboxStatus() { 19 | this.props.dispatch(getUnboxStatus(this.props.unbox.tradeId)); 20 | } 21 | 22 | render() { 23 | return null; 24 | } 25 | } 26 | 27 | export default connect(state => ({ 28 | unbox: state.unbox, 29 | }))(UnboxPoller); 30 | -------------------------------------------------------------------------------- /client/src/modules/error.js: -------------------------------------------------------------------------------- 1 | const error = (state = { error: false }, action) => { 2 | switch (action.type) { 3 | case 'GET_AVAILABLE_KEYS_ERROR': 4 | return { 5 | error: true, 6 | message: 7 | 'To use vCase.gg, you must first log in to trade.opskins.com with your Steam ID. Then log in to vCase.gg with the same Steam ID', 8 | }; 9 | case 'GET_CASES_ERROR': 10 | return { 11 | error: true, 12 | message: 13 | 'We cannot get the list of cases from the server. Please refresh the page and try again.', 14 | }; 15 | case 'EXIT': 16 | return { error: false }; 17 | default: 18 | return state; 19 | } 20 | }; 21 | 22 | export default error; 23 | -------------------------------------------------------------------------------- /client/src/containers/header/error-listener.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { logout } from '../../actions'; 4 | import ErrorPopup from './error'; 5 | 6 | class ErrorListener extends Component { 7 | goBack(event) { 8 | event.preventDefault(); 9 | this.props.dispatch(logout()); 10 | } 11 | 12 | render() { 13 | if (this.props.error) { 14 | return ( 15 | 19 | ); 20 | } 21 | return null; 22 | } 23 | } 24 | 25 | export default connect(state => ({ 26 | error: state.error.error, 27 | message: state.error.message, 28 | }))(ErrorListener); 29 | -------------------------------------------------------------------------------- /client/src/containers/home/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import CaseList from '../cases'; 4 | import UnboxStatus from '../unbox'; 5 | import FixedHeader from '../header/fixed'; 6 | 7 | function TopPanel(props) { 8 | if (props.unbox.state !== 'NOT_STARTED') { 9 | return ( 10 | 11 | 12 | 13 | 14 | ); 15 | } else { 16 | return ( 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | } 24 | 25 | class Home extends Component { 26 | render() { 27 | return TopPanel(this.props); 28 | } 29 | } 30 | 31 | export default connect(state => ({ 32 | unbox: state.unbox, 33 | }))(Home); 34 | -------------------------------------------------------------------------------- /client/src/modules/cases.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const individualCase = (state, action) => { 4 | switch (action.type) { 5 | case 'GET_CASE_ITEMS_RECEIVED': 6 | if (state.id == action.caseId) { 7 | return Object.assign({}, state, { items: action.items }); 8 | } else { 9 | return state; 10 | } 11 | default: 12 | return state; 13 | } 14 | }; 15 | 16 | const cases = (state = [], action) => { 17 | switch (action.type) { 18 | case 'GET_CASES_RECEIVED': 19 | return action.data; 20 | case 'GET_CASE_ITEMS_RECEIVED': 21 | return state.map(kase => { 22 | return individualCase(kase, action); 23 | }); 24 | case 'SHUFFLE_CASES': 25 | return _.shuffle(state); 26 | default: 27 | return state; 28 | } 29 | }; 30 | 31 | export default cases; 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | client/node_modules: 2 | @cd client && npm install 3 | 4 | vgomock/node_modules: 5 | @cd vgomock && npm install 6 | 7 | node_modules: 8 | @npm install 9 | 10 | .PHONY:prettier 11 | prettier: build 12 | @npm run prettier 13 | 14 | .PHONY:lint-js 15 | lint: build 16 | @npm run lint 17 | 18 | .PHONY: dev 19 | dev: build 20 | @npm run dev 21 | 22 | .PHONY: vgo-mock 23 | vgo-mock: vgomock/node_modules 24 | @npm run vgo-mock 25 | 26 | .PHONY: build 27 | build: client/node_modules node_modules 28 | 29 | .PHONY: rebuild 30 | rebuild: clean build 31 | 32 | .PHONY: clean 33 | clean: 34 | @rm -rf node_modules 35 | @rm -rf client/node_modules 36 | @rm -rf vgomock/node_modules 37 | 38 | .PHONY: static-files 39 | static-files: client/node_modules 40 | @rm -rf public/* 41 | @cd client && npm run build 42 | @mv client/build/* public/ 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 vgoskins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/src/containers/unbox/item.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class CaseItem extends Component { 4 | render() { 5 | let visibilityClass = this.props.visibility || ''; 6 | 7 | return ( 8 |
9 |
10 |
11 | 12 |
13 |
14 |

18 | {this.props.name} 19 |

20 |

21 | {this.props.wearTier ? ( 22 | {this.props.wearTier} 23 | ) : null} 24 | {this.props.category} 25 |

26 |
27 |
28 |
29 | ); 30 | } 31 | } 32 | 33 | export default CaseItem; 34 | -------------------------------------------------------------------------------- /client/src/containers/app/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | import Home from '../home'; 4 | import Login from '../login'; 5 | import Info from '../info'; 6 | import LoginCallback from '../login/callback'; 7 | import { connect } from 'react-redux'; 8 | import { withRouter } from 'react-router'; 9 | 10 | class App extends Component { 11 | render() { 12 | let routes; 13 | if (this.props.authenticated) { 14 | routes = ( 15 | 16 | 17 | 18 | 19 | ); 20 | } else { 21 | routes = ( 22 | 23 | 24 | 25 | ); 26 | } 27 | return ( 28 | 29 | 30 | 31 | {routes} 32 | 33 | 34 | ); 35 | } 36 | } 37 | 38 | export default withRouter( 39 | connect(state => ({ 40 | authenticated: state.auth.authenticated, 41 | }))(App) 42 | ); 43 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Open vCases, Unlock VGO vSkins | VCase.gg 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 |
24 |
25 |
26 |
27 | 28 |

© 2018 VCASE™. All rights reserved.

29 |
30 | 31 |
32 |
33 |
34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /client/src/containers/developers/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Developers extends Component { 4 | render() { 5 | return ( 6 |
7 |
13 |
14 |
15 |

Developers

16 |

17 | Create your own vCase site using our open source code. Visit 18 | GitHub to get started. 19 |

20 |
21 |
22 | 23 |
24 | 33 |
34 |
35 |
36 | ); 37 | } 38 | } 39 | 40 | export default Developers; 41 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vcase", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.18.0", 7 | "bootstrap": "^4.1.1", 8 | "font-awesome": "^4.7.0", 9 | "history": "^4.7.2", 10 | "jquery": "1.9.1 - 3", 11 | "lodash": "^4.17.5", 12 | "popper.js": "^1.14.3", 13 | "qs": "^6.5.2", 14 | "react": "^16.3.2", 15 | "react-dom": "^16.3.2", 16 | "react-redux": "^5.0.7", 17 | "react-router-dom": "^4.2.2", 18 | "react-router-redux": "^5.0.0-alpha.9", 19 | "react-scripts": "1.1.4", 20 | "reactstrap": "^6.0.1", 21 | "redux": "^4.0.0", 22 | "redux-devtools-extension": "^2.13.2", 23 | "redux-persist": "^5.9.1", 24 | "redux-thunk": "^2.2.0", 25 | "simple-line-icons": "^2.4.1" 26 | }, 27 | "proxy": { 28 | "/auth*": { 29 | "target": "http://localhost:3001" 30 | }, 31 | "/auth/completed": { 32 | "target": "http://localhost:3001" 33 | }, 34 | "/cases*": { 35 | "target": "http://localhost:3001" 36 | }, 37 | "/keys": { 38 | "target": "http://localhost:3001" 39 | }, 40 | "/offer/*": { 41 | "target": "http://localhost:3001" 42 | }, 43 | "/items": { 44 | "target": "http://localhost:3001" 45 | } 46 | }, 47 | "scripts": { 48 | "start": "react-scripts start", 49 | "build": "react-scripts build", 50 | "test": "react-scripts test --env=jsdom", 51 | "eject": "react-scripts eject" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/src/containers/unbox/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import UnboxWaiting from './waiting'; 4 | import ErrorPopup from '../header/error'; 5 | import UnboxConfirmation from './confirmation'; 6 | import UnboxResult from './result'; 7 | import UnboxPoller from './poller'; 8 | import { endUnboxing } from '../../actions'; 9 | 10 | class UnboxStatus extends Component { 11 | goBack(event) { 12 | event.preventDefault(); 13 | this.props.dispatch(endUnboxing()); 14 | } 15 | 16 | render() { 17 | switch (this.props.unbox.state) { 18 | case 'OFFER_PENDING': 19 | return ( 20 | 21 | 22 | 23 | 24 | ); 25 | case 'OFFER_FAILED': 26 | case 'OPENING_FAILED': 27 | return ( 28 | 32 | ); 33 | case 'OPENING_COMPLETED': 34 | case 'OPENING_PARTIAL_FAILURE': 35 | return ; 36 | default: 37 | return ( 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | } 45 | } 46 | 47 | export default connect(state => ({ 48 | unbox: state.unbox, 49 | }))(UnboxStatus); 50 | -------------------------------------------------------------------------------- /client/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import { routerMiddleware } from 'react-router-redux'; 3 | import thunk from 'redux-thunk'; 4 | import createHistory from 'history/createBrowserHistory'; 5 | import { persistStore, persistReducer } from 'redux-persist'; 6 | import storage from 'redux-persist/lib/storage'; 7 | import rootReducer from './modules'; 8 | import { authStatus } from './actions'; 9 | 10 | export const history = createHistory(); 11 | 12 | const initialState = {}; 13 | const enhancers = []; 14 | const middleware = [routerMiddleware(history)]; 15 | 16 | if (process.env.NODE_ENV === 'development') { 17 | const devToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION__; 18 | 19 | if (typeof devToolsExtension === 'function') { 20 | enhancers.push(devToolsExtension()); 21 | } 22 | } 23 | 24 | const persistConfig = { 25 | key: 'root', 26 | storage, 27 | }; 28 | 29 | const persistedReducer = persistReducer(persistConfig, rootReducer); 30 | 31 | const composedEnhancers = compose(applyMiddleware(...middleware), ...enhancers); 32 | 33 | const store = createStore( 34 | persistedReducer, 35 | applyMiddleware(thunk), 36 | composedEnhancers, 37 | initialState 38 | ); 39 | 40 | persistStore(store); 41 | 42 | window.addEventListener( 43 | 'storage', 44 | function(event) { 45 | if (event.key === 'AUTHENTICATION_STATUS') { 46 | let auth = JSON.parse(event.newValue); 47 | store.dispatch( 48 | authStatus(auth.authenticated, auth.username, auth.avatar) 49 | ); 50 | } 51 | }, 52 | false 53 | ); 54 | 55 | export default store; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VCase", 3 | "version": "0.0.1", 4 | "description": "The VGo Unboxing Website", 5 | "main": "server.js", 6 | "engines": { 7 | "node": "8.9.4" 8 | }, 9 | "dependencies": { 10 | "axios": "^0.18.0", 11 | "bluebird": "^3.5.1", 12 | "bunyan": "^1.8.12", 13 | "istanbul": "^0.4.5", 14 | "koa": "^2.5.0", 15 | "koa-bodyparser": "^4.2.0", 16 | "koa-bunyan": "^1.0.1", 17 | "koa-bunyan-logger": "^2.0.0", 18 | "koa-passport": "^4.1.0", 19 | "koa-qs": "^2.0.0", 20 | "koa-rewrite": "^3.0.1", 21 | "koa-router": "^7.4.0", 22 | "koa-session": "^5.8.1", 23 | "koa-static": "^4.0.2", 24 | "lodash": "^4.17.5", 25 | "omit-deep": "^0.3.0", 26 | "passport-steam": "^1.0.10", 27 | "qs": "^6.5.2" 28 | }, 29 | "devDependencies": { 30 | "concurrently": "^3.5.1", 31 | "mocha": "^5.0.1", 32 | "prettier": "1.10.2", 33 | "should": "^13.2.1" 34 | }, 35 | "scripts": { 36 | "dev": "NODE_ENV=development npm start", 37 | "start": "concurrently \"npm run server\" \"npm run client\" \"npm run vgo-mock\"", 38 | "server": "node server.js", 39 | "client": "node start-client.js", 40 | "vgo-mock": "node start-vgo-mock.js", 41 | "test": "NODE_ENV=test ./node_modules/.bin/istanbul cover _mocha -- --timeout 5000 --require should --exit 'test/**/*.js'", 42 | "lint": "./node_modules/.bin/prettier --single-quote --trailing-comma es5 --list-different {./,**/,**/**/,**/**/**}*.js", 43 | "prettier": "./node_modules/.bin/prettier --single-quote --trailing-comma es5 --write {./,**/,**/**/,**/**/**}*.js" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/src/containers/header/error.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | class ErrorPopup extends Component { 5 | render() { 6 | return ( 7 |
8 | 38 |
39 |
40 | ); 41 | } 42 | } 43 | 44 | export default connect()(ErrorPopup); 45 | -------------------------------------------------------------------------------- /client/src/containers/info/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import FloatingHeader from '../header/floating'; 4 | import { Link } from 'react-router-dom'; 5 | import FaqList from '../faq'; 6 | import Developers from '../developers'; 7 | 8 | class Info extends Component { 9 | render() { 10 | return ( 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 | OPEN CASE 21 | 22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | 44 | 45 | ); 46 | } 47 | } 48 | 49 | export default connect()(Info); 50 | -------------------------------------------------------------------------------- /client/src/containers/cases/remaining-keys.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { getAvailableKeys } from '../../actions'; 4 | import $ from 'jquery'; 5 | 6 | class RemainingKeys extends Component { 7 | componentDidMount() { 8 | this.props.dispatch(getAvailableKeys()); 9 | $(document).ready(function() { 10 | $('[data-toggle="tooltip"]').tooltip(); 11 | }); 12 | } 13 | 14 | render() { 15 | const tooltipText = 16 | 'How do I get vKeys?

' + 17 | 'There are two ways to get vKeys, which are used to open vCases:

' + 18 | '
    ' + 19 | '
  • Purchase vKeys from a marketplace that supports VGO items
  • ' + 20 | '
  • Receive vKeys in a trade from another VGO user
  • ' + 21 | '
'; 22 | 23 | return ( 24 | 25 |
26 |
27 | {this.props.keys.loaded ? this.props.keys.available : ''} 28 |
29 |
30 |

VKeys

31 | Remaining 32 |
33 |
34 | 46 |
47 | ); 48 | } 49 | } 50 | 51 | export default connect(state => ({ 52 | keys: state.keys, 53 | }))(RemainingKeys); 54 | -------------------------------------------------------------------------------- /client/src/containers/login/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import LoginButton from './button'; 4 | import { getAuthStatus } from '../../actions'; 5 | import FloatingHeader from '../header/floating'; 6 | import FaqList from '../faq'; 7 | import Developers from '../developers'; 8 | 9 | class Login extends Component { 10 | componentDidMount() { 11 | this.props.dispatch(getAuthStatus()); 12 | } 13 | 14 | render() { 15 | return ( 16 | 17 | 18 |
19 |
20 |
21 |
22 |
23 |
Sign in to start
24 |
25 | 26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 | 49 | ); 50 | } 51 | } 52 | 53 | export default connect(state => ({ 54 | authenticated: state.auth.authenticated, 55 | }))(Login); 56 | -------------------------------------------------------------------------------- /client/public/img/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /client/src/containers/unbox/confirmation.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { endUnboxing } from '../../actions'; 4 | 5 | class UnboxConfirmation extends Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | cancel(event) { 11 | event.preventDefault(); 12 | this.props.dispatch(endUnboxing()); 13 | } 14 | 15 | render() { 16 | return ( 17 |
18 | 51 |
52 |
53 | ); 54 | } 55 | } 56 | 57 | export default connect(state => ({ 58 | unbox: state.unbox, 59 | }))(UnboxConfirmation); 60 | -------------------------------------------------------------------------------- /client/src/modules/unbox.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Unbox States: 3 | * 4 | * NOT_STARTED: There is no unboxing in process 5 | * OFFER_PENDING: The process started and we are waiting the user to accept the offer 6 | * OFFER_FAILED: The offer failed for some reason (the process cannot continue) 7 | * OPENING_PENDING: The offer was accepted and we are waiting the cases to be opened 8 | * OPENING_FAILED: The opening process failed (the process cannot continue) 9 | * OPENING_COMPLETED: The opening process finished correctly 10 | * OPENING_PARTIAL_FAILURE: Some cases were opened and some failed 11 | */ 12 | const INITIAL_STATE = { 13 | state: 'NOT_STARTED', 14 | items: [], 15 | totalExpectedItems: 0, 16 | }; 17 | 18 | const unbox = (state = INITIAL_STATE, action) => { 19 | switch (action.type) { 20 | case 'UNBOX': 21 | return { state: 'NOT_STARTED', caseId: action.caseId }; 22 | case 'UNBOX_RECEIVED': 23 | return Object.assign({}, state, { 24 | state: 'OFFER_PENDING', 25 | tradeId: action.tradeId, 26 | tradeOfferUrl: action.tradeOfferUrl, 27 | }); 28 | case 'UNBOX_ERROR': 29 | return Object.assign({}, state, { state: 'OFFER_FAILED' }); 30 | case 'GET_UNBOX_STATUS_RECEIVED': 31 | return Object.assign({}, state, { 32 | state: getNewState(action.offerState, action.openingState), 33 | items: action.items, 34 | totalExpectedItems: action.totalExpectedItems, 35 | }); 36 | case 'END_UNBOXING': 37 | case 'EXIT': 38 | case 'AUTH_STATUS': 39 | return INITIAL_STATE; 40 | default: 41 | return state; 42 | } 43 | }; 44 | 45 | function getNewState(offerState, openingState) { 46 | if (offerState === 0) { 47 | return 'OFFER_PENDING'; 48 | } else if (offerState === -1) { 49 | return 'OFFER_FAILED'; 50 | } else if (openingState === -1) { 51 | return 'OPENING_FAILED'; 52 | } else if (openingState === 1) { 53 | return 'OPENING_COMPLETED'; 54 | } else if (openingState === -2) { 55 | return 'OPENING_PARTIAL_FAILURE'; 56 | } else { 57 | return 'OPENING_PENDING'; 58 | } 59 | } 60 | 61 | export default unbox; 62 | -------------------------------------------------------------------------------- /client/src/containers/cases/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Case from './case'; 4 | import RemainingKeys from './remaining-keys'; 5 | import { getCases, unbox } from '../../actions'; 6 | import ShuffleButton from './shuffle'; 7 | import AutoPick from './autopick'; 8 | 9 | class CaseList extends Component { 10 | constructor(props) { 11 | super(props); 12 | } 13 | 14 | componentWillMount() { 15 | this.props.dispatch(getCases()); 16 | } 17 | 18 | isUnboxing() { 19 | return this.props.unbox.state === 'IN_PROGRESS'; 20 | } 21 | 22 | unbox(kase, amount) { 23 | if (this.isUnboxing()) { 24 | return; // For now, can only unbox one at a time 25 | } 26 | this.props.dispatch(unbox(kase, amount)); 27 | } 28 | 29 | render() { 30 | return ( 31 |
32 |
33 |
34 | 35 |
36 | 37 |
38 | 39 |
40 |
41 |
42 |
43 | {this.props.cases.map((props, i) => ( 44 |
49 | 54 |
55 | ))} 56 |
57 |
58 | 59 |
60 | ); 61 | } 62 | } 63 | 64 | export default connect(state => ({ 65 | cases: state.cases, 66 | unbox: state.unbox, 67 | tradeUrl: state.tradeUrl, 68 | keys: state.keys, 69 | }))(CaseList); 70 | -------------------------------------------------------------------------------- /client/src/containers/header/nav-user.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { UncontrolledDropdown, DropdownToggle, DropdownMenu } from 'reactstrap'; 4 | import { logout } from '../../actions'; 5 | import { Link } from 'react-router-dom'; 6 | import $ from 'jquery'; 7 | 8 | class NavUser extends Component { 9 | exitClicked(event) { 10 | event.preventDefault(); 11 | this.props.dispatch(logout()); 12 | } 13 | 14 | componentDidMount() { 15 | $('.nav-user').hover( 16 | () => { 17 | $('.nav-user .dropdown-menu').addClass('show'); 18 | }, 19 | () => { 20 | $('.nav-user .dropdown-menu').removeClass('show'); 21 | } 22 | ); 23 | } 24 | 25 | componentWillUnmount() { 26 | $('.nav-user').off('mouseenter mouseleave'); 27 | } 28 | 29 | render() { 30 | let isHomeLink; 31 | if (!this.props.isHome) { 32 | isHomeLink = ( 33 | 34 | Return to Home 35 | 36 | ); 37 | } 38 | return ( 39 | 40 | 41 | 42 | 47 | {this.props.auth.username} 48 | 49 | 50 | 55 | Sign out 56 | 57 | {isHomeLink} 58 | 63 | My VGO Inventory 64 | 65 | 66 | 67 | 68 | ); 69 | } 70 | } 71 | 72 | export default connect(state => ({ 73 | auth: state.auth, 74 | }))(NavUser); 75 | -------------------------------------------------------------------------------- /client/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 16 | 22 | 25 | 33 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /client/src/containers/header/floating.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ErrorListener from './error-listener'; 4 | import NavUser from './nav-user'; 5 | import LoginButton from '../login/button'; 6 | import $ from 'jquery'; 7 | import { Link } from 'react-router-dom'; 8 | 9 | class FloatingHeader extends Component { 10 | constructor() { 11 | super(); 12 | this.handleScroll = this.handleScroll.bind(this); 13 | this.state = { 14 | visible: false, 15 | }; 16 | } 17 | 18 | handleScroll() { 19 | const doc = document.documentElement; 20 | const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); 21 | if ( 22 | top > 23 | $('#home-button-container').offset().top + 24 | $('#home-button-container').height() 25 | ) { 26 | this.setState({ visible: true }); 27 | } else { 28 | this.setState({ visible: false }); 29 | } 30 | } 31 | 32 | componentDidMount() { 33 | window.addEventListener('scroll', this.handleScroll); 34 | } 35 | 36 | componentWillUnmount() { 37 | window.removeEventListener('scroll', this.handleScroll); 38 | } 39 | 40 | render() { 41 | let button; 42 | let divider; 43 | let navbarClassName = 'navbar navbar-static-top fixed-top'; 44 | if (this.props.authenticated) { 45 | navbarClassName += ' hide-bg-and-logo'; 46 | if (this.state.visible) { 47 | divider =
; 48 | button = ( 49 | 50 | OPEN CASE 51 | 52 | ); 53 | } 54 | } else { 55 | button = ; 56 | } 57 | if (this.state.visible) { 58 | navbarClassName += ' visible'; 59 | } 60 | return ( 61 | 74 | ); 75 | } 76 | } 77 | 78 | export default connect(state => ({ 79 | authenticated: state.auth.authenticated, 80 | }))(FloatingHeader); 81 | -------------------------------------------------------------------------------- /client/src/containers/unbox/waiting.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { getCaseItems } from '../../actions'; 4 | import CaseItem from './item'; 5 | 6 | class UnboxWaiting extends Component { 7 | componentDidMount() { 8 | this.props.dispatch(getCaseItems(this.getCase())); 9 | } 10 | 11 | getCase() { 12 | return this.props.cases.find(kase => { 13 | return kase.id == this.props.caseId; 14 | }); 15 | } 16 | 17 | render() { 18 | let items = []; 19 | if (this.getCase() && this.getCase().items) { 20 | items = this.getCase().items; 21 | } 22 | 23 | // Initialize the slider items list and indicators 24 | const sliderItems = []; 25 | const sliderIndicators = []; 26 | 27 | // For each item add it to the list and add an indicator 28 | items.forEach((item, index) => { 29 | sliderItems.push( 30 |
34 | 35 |
36 |

{item.name}

37 |

{item.category}

38 |
39 |
40 | ); 41 | 42 | sliderIndicators.push( 43 |
  • 49 | ); 50 | }); 51 | 52 | return ( 53 |
    54 |
    55 |
    56 |

    57 | Generating your items on the blockchain. Please give us two 58 | minutes 59 |

    60 |
    UNLOCKING YOUR ITEMS
    61 | 62 |
    63 |
    64 |
    65 |

    Items that might be in this case

    66 |
    67 |
    68 |
    69 | {items.map((item, i) => ( 70 | 77 | ))} 78 |
    79 |
    80 |
    81 | ); 82 | } 83 | } 84 | 85 | export default connect(state => ({ 86 | cases: state.cases, 87 | }))(UnboxWaiting); 88 | -------------------------------------------------------------------------------- /client/src/containers/unbox/result.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { endUnboxing } from '../../actions'; 4 | import CaseItem from './item'; 5 | 6 | const INTERVAL_MS = 1000; 7 | 8 | class UnboxResult extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.openCases = this.props.unbox.items.length; 12 | this.state = { 13 | shownItems: 0, 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | this.interval = setInterval(() => { 19 | this.showOneMoreItem(); 20 | }, INTERVAL_MS); 21 | } 22 | 23 | componentWillUnmount() { 24 | this.clearInterval(); 25 | } 26 | 27 | clearInterval() { 28 | if (this.interval) { 29 | clearInterval(this.interval); 30 | this.interval = null; 31 | } 32 | } 33 | 34 | showOneMoreItem() { 35 | if (this.state.shownItems === this.openCases) { 36 | this.clearInterval(); 37 | } 38 | this.setState({ shownItems: this.state.shownItems + 1 }); 39 | } 40 | 41 | goBack() { 42 | // Update unbox status 43 | this.props.dispatch(endUnboxing()); 44 | } 45 | 46 | render() { 47 | let text = `YOU GOT ${this.openCases} NEW ITEMS!`; 48 | if (this.props.unbox.state === 'OPENING_PARTIAL_FAILURE') { 49 | let failed = this.props.unbox.totalExpectedItems - this.openCases; 50 | text = `${ 51 | this.openCases 52 | } cases successfully opened! ${failed} case openings did not complete, and those keys were not deducted from your balance. Please try opening ${failed} again.`; 53 | } 54 | const items = this.props.unbox.items; 55 | 56 | return ( 57 |
    58 |
    59 |
    60 |
    61 |

    {text}

    62 |
    63 |
    64 |
    65 |
    66 | 73 |
    74 |
    75 |
    76 |
    77 | {items.map((item, i) => ( 78 | i ? 'fadein' : 'hidden'} 86 | /> 87 | ))} 88 |
    89 |
    90 |
    91 | ); 92 | } 93 | } 94 | 95 | export default connect(state => ({ 96 | unbox: state.unbox, 97 | }))(UnboxResult); 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VCase 2 | 3 | The VCase website which facilitates VGo case unboxing and selection for users. 4 | 5 | ## Overview 6 | 7 | This react-redux app uses a [separated client and server, servers, architecture](https://www.fullstackreact.com/articles/using-create-react-app-with-a-server/). 8 | 9 | The client aspect is a standalone redux server and all of that is contained within the `client` folder. 10 | 11 | The server code exists at root level and may soon be moved to a `server` folder. 12 | 13 | ## Development 14 | 15 | You will need Node.js >= 8.9.4 `nvm install 8.9.4` 16 | 17 | Install dependencies 18 | 19 | ``` 20 | $ make build 21 | ``` 22 | 23 | Launch the backend API server and the front end client server in one shot. 24 | 25 | ``` 26 | $ STEAM_API_KEY="STEAM API KEY" make dev 27 | ``` 28 | 29 | Also run the VGo mock server for a simple local development flow. 30 | 31 | ``` 32 | $ make vgo-mock 33 | ``` 34 | 35 | ## Production 36 | 37 | You will need Node.js >= 8.9.4 `nvm install 8.9.4` 38 | 39 | Install dependencies 40 | 41 | ``` 42 | $ make build 43 | ``` 44 | 45 | Generate static files 46 | 47 | ``` 48 | $ make static-files 49 | ``` 50 | 51 | Run the server 52 | 53 | ``` 54 | $ NODE_ENV=production STEAM_API_KEY="STEAM API KEY" BASE_URL="http://vcase.gg" SESSION_KEY="SOME_RANDOM_KEY" PORT=3001 VGO_URL="https://api-trade.opskins.com" VGO_API_KEY="SOME_API_KEY" AFFILIATE_ADDRESS="0x939826f5acff002bf6b898fb8151cac83b2401" npm run server 55 | ``` 56 | 57 | ### Configuration variables 58 | 59 | * NODE_ENV: It allows the usage of default values for development and QA environments. It needs to be set to 'production' for any other environment including staging. 60 | * STEAM_API_KEY: This is the key used for login integration with steam. It can be generated here https://steamcommunity.com/dev/apikey 61 | * BASE_URL: The URL where this website is available (will be used to construct redirect URLs from external services) 62 | * SESSION_KEY: Random base64 key used for signing and verifying the cookie used to store session data. A secure key can be generated by running: *openssl rand -base64 32* 63 | * PORT: The port where the application will listen for HTTP requests 64 | * VGO_URL: URL of the WAX ExpressTrade API. 65 | * VGO_API_KEY: VCase API key generated from [IUser/CreateVCaseUser](https://github.com/OPSkins/trade-opskins-api/blob/master/IUser/CreateVCaseUser.md) 66 | * AFFILIATE_ADDRESS: Ethereum address of the account to be used for collecting payments for openings generated from this site. 67 | 68 | ## Monitoring 69 | 70 | The application provides a simple health check endpoint that can be used to monitor the status of the app. 71 | 72 | ``` 73 | $ curl http://localhost:3001/health 74 | {"api":"up"} 75 | ``` 76 | 77 | Given that this project has no external dependencies other than the VGO API the health of the nodejs server itself is the only thing that needs to be monitored. 78 | 79 | The server logs to the standard output by default. 80 | 81 | ## Tutorials 82 | 83 | * https://medium.com/@notrab/getting-started-with-create-react-app-redux-react-router-redux-thunk-d6a19259f71f 84 | * https://www.fullstackreact.com/articles/using-create-react-app-with-a-server/ 85 | * https://github.com/facebook/create-react-app 86 | * https://www.sohamkamani.com/blog/2016/06/05/redux-apis/ 87 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const assert = require('assert'); 3 | const omit = require('omit-deep'); 4 | const environment = process.env.NODE_ENV; 5 | 6 | const configs = { 7 | production: function() { 8 | return { 9 | baseURL: assertEnvVar( 10 | 'BASE_URL', 11 | 'The URL of the website including the protocol and the port if needed' 12 | ), 13 | port: assertEnvVar( 14 | 'PORT', 15 | 'The port that the REST api should be listening on' 16 | ), 17 | vgoURL: assertEnvVar( 18 | 'VGO_URL', 19 | 'The VGO URL used for getting cases information and sending opening offers' 20 | ), 21 | vgoAPIKey: assertEnvVar('VGO_API_KEY', 'The VGO website API Key'), 22 | affiliateAddress: assertEnvVar( 23 | 'AFFILIATE_ADDRESS', 24 | 'The Ethereum address of the account for collecting fees' 25 | ), 26 | sessionKeys: [ 27 | assertEnvVar('SESSION_KEY', 'Key used for session managment'), 28 | ], 29 | steamApiKey: assertEnvVar( 30 | 'STEAM_API_KEY', 31 | 'Key used for steam login integration' 32 | ), 33 | log: { 34 | level: optionalEnvVar('LOG_LEVEL', 'Log level', 'info'), 35 | }, 36 | }; 37 | }, 38 | development: function() { 39 | return { 40 | baseURL: 'http://localhost:3000', 41 | port: 3001, 42 | vgoURL: 'http://localhost:3002', 43 | vgoAPIKey: 'somekey', 44 | affiliateAddress: '0x0000000000000000', 45 | sessionKeys: ['this is not secure'], 46 | steamApiKey: assertEnvVar( 47 | 'STEAM_API_KEY', 48 | 'Key used for steam login integration' 49 | ), 50 | log: { 51 | level: 'debug', 52 | }, 53 | }; 54 | }, 55 | qa: function() { 56 | return { 57 | baseURL: 'http://localhost:3000', 58 | port: 3001, 59 | vgoURL: 'http://localhost:3002', 60 | vgoAPIKey: 'somekey', 61 | affiliateAddress: '0x0000000000000000', 62 | sessionKeys: ['this is not secure'], 63 | steamApiKey: assertEnvVar( 64 | 'STEAM_API_KEY', 65 | 'Key used for steam login integration' 66 | ), 67 | log: { 68 | level: 'debug', 69 | }, 70 | }; 71 | }, 72 | }; 73 | 74 | let config = configs[environment]; 75 | 76 | assert( 77 | config, 78 | `Configuration ${environment} does not exist, NODE_ENV must be one of ${Object.keys( 79 | configs 80 | )}` 81 | ); 82 | 83 | config = config(); 84 | 85 | // Convenience helper to display the config with sensitive fields stripped 86 | config.toWire = function() { 87 | return omit(_.cloneDeep(config), ['password', 'toWire']); 88 | }; 89 | 90 | console.log( 91 | `Running in ${environment} mode. Config is ${JSON.stringify( 92 | config.toWire(), 93 | null, 94 | 2 95 | )}\n--\n'password' keys omitted.` 96 | ); 97 | 98 | module.exports = config; 99 | 100 | function assertEnvVar(envVar, description) { 101 | const value = process.env[envVar]; 102 | assert( 103 | value !== null && value !== undefined, 104 | `Environment variable ${envVar} (currently is ${value}) must be present. Description: ${description}` 105 | ); 106 | return value; 107 | } 108 | 109 | function optionalEnvVar(envVar, description, defaultValue) { 110 | const value = process.env[envVar]; 111 | if (value === null || value === undefined) { 112 | if (defaultValue === undefined) { 113 | console.warn( 114 | `Optional environment variable ${envVar} (currently is ${value}) not present. Description: ${description}` 115 | ); 116 | } else { 117 | console.warn( 118 | `Optional environment variable ${envVar} (currently is ${value}) not present. Description: ${description}. Using default value ${defaultValue}` 119 | ); 120 | } 121 | } 122 | return value || defaultValue; 123 | } 124 | -------------------------------------------------------------------------------- /client/src/containers/cases/case.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { Button, Input } from 'reactstrap'; 3 | import { connect } from 'react-redux'; 4 | import ErrorPopup from '../header/error'; 5 | 6 | class Case extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | quantity: this.props.keys.minimum, 11 | showErrorMessage: false, 12 | }; 13 | } 14 | 15 | quantityChanged(event) { 16 | event.preventDefault(); 17 | const selectedQuantity = parseInt(event.target.value, 10); 18 | if ( 19 | selectedQuantity >= this.props.keys.minimum && 20 | selectedQuantity <= this.props.keys.available 21 | ) { 22 | this.setState({ quantity: selectedQuantity }); 23 | } 24 | } 25 | 26 | unbox() { 27 | if ( 28 | this.state.quantity >= this.props.keys.minimum && 29 | this.state.quantity <= this.props.keys.available 30 | ) { 31 | this.setState({ showErrorMessage: false }); 32 | this.props.unbox(this.props.case.id, this.state.quantity); 33 | } else { 34 | this.setState({ showErrorMessage: true }); 35 | } 36 | } 37 | 38 | onCaseDetailClicked() { 39 | this.props.onCaseDetailClicked(this.props.case.id); 40 | } 41 | 42 | addOne(event) { 43 | event.preventDefault(); 44 | if (this.state.quantity < this.props.keys.available) { 45 | this.setState({ quantity: this.state.quantity + 1 }); 46 | } 47 | } 48 | 49 | removeOne(event) { 50 | event.preventDefault(); 51 | if (this.state.quantity > this.props.keys.minimum) { 52 | this.setState({ quantity: this.state.quantity - 1 }); 53 | } 54 | } 55 | 56 | cancel() { 57 | this.setState({ showErrorMessage: false }); 58 | } 59 | 60 | render() { 61 | let errorPopup = null; 62 | if (this.state.showErrorMessage) { 63 | errorPopup = ( 64 | 72 | ); 73 | } 74 | 75 | return ( 76 | 77 | {errorPopup} 78 |
    79 |
    80 | {this.props.case.name} { 84 | e.target.src = 'img/default-vcase.png'; 85 | }} 86 | className="card-img-top" 87 | /> 88 |

    {this.props.case.name}

    89 |
    90 | 91 |
    92 |
    93 | 94 | 100 | 101 | 107 | 108 | 114 | 115 |
    116 | 123 |
    124 |
    125 |
    126 | ); 127 | } 128 | } 129 | 130 | export default connect(state => ({ 131 | keys: state.keys, 132 | }))(Case); 133 | -------------------------------------------------------------------------------- /client/src/containers/cases/autopick.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import _ from 'lodash'; 4 | import ErrorPopup from '../header/error'; 5 | 6 | class AutoPick extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | quantity: this.props.keys.minimum, 11 | showErrorMessage: false, 12 | }; 13 | } 14 | 15 | quantityChanged(event) { 16 | event.preventDefault(); 17 | const selectedQuantity = parseInt(event.target.value, 10); 18 | if ( 19 | selectedQuantity >= this.props.keys.minimum && 20 | selectedQuantity <= this.props.keys.available 21 | ) { 22 | this.setState({ quantity: selectedQuantity }); 23 | } 24 | } 25 | 26 | addOne(event) { 27 | event.preventDefault(); 28 | if (this.state.quantity < this.props.keys.available) { 29 | this.setState(prevState => ({ quantity: prevState.quantity + 1 })); 30 | } 31 | } 32 | 33 | removeOne(event) { 34 | event.preventDefault(); 35 | if (this.state.quantity > this.props.keys.minimum) { 36 | this.setState(prevState => ({ quantity: prevState.quantity - 1 })); 37 | } 38 | } 39 | 40 | autoPick() { 41 | if ( 42 | this.state.quantity >= this.props.keys.minimum && 43 | this.state.quantity <= this.props.keys.available 44 | ) { 45 | // Pick one random case from the list 46 | const selectedCase = _.sample(this.props.cases); 47 | this.setState({ showErrorMessage: false }); 48 | // Unbox the selected case with the selected quantity 49 | this.props.unbox(selectedCase.id, this.state.quantity); 50 | } else { 51 | this.setState({ showErrorMessage: true }); 52 | } 53 | } 54 | 55 | cancel() { 56 | this.setState({ showErrorMessage: false }); 57 | } 58 | 59 | render() { 60 | let errorPopup = null; 61 | if (this.state.showErrorMessage) { 62 | errorPopup = ( 63 | 71 | ); 72 | } 73 | return ( 74 | 75 | {errorPopup} 76 |
    83 |
    84 |

    Auto-Pick Any Case

    85 |
    86 | Quantity 87 |
    88 | 89 | 96 | 97 | 103 | 104 | 111 | 112 |
    113 | 120 |
    121 |
    122 |
    123 |
    124 | ); 125 | } 126 | } 127 | 128 | export default connect(state => ({ 129 | keys: state.keys, 130 | cases: state.cases, 131 | }))(AutoPick); 132 | -------------------------------------------------------------------------------- /client/src/actions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import qs from 'qs'; 3 | 4 | function storeInStorage(key, value) { 5 | localStorage.setItem(key, JSON.stringify(value)); 6 | } 7 | 8 | function getCases() { 9 | return dispatch => { 10 | dispatch({ type: 'GET_CASES' }); 11 | 12 | return axios 13 | .get('/cases') 14 | .then(res => { 15 | return dispatch({ 16 | type: `GET_CASES_RECEIVED`, 17 | data: res.data, 18 | }); 19 | }) 20 | .catch(error => { 21 | return dispatch({ 22 | type: `GET_CASES_ERROR`, 23 | error, 24 | }); 25 | }); 26 | }; 27 | } 28 | 29 | function unbox(caseId, amount) { 30 | return dispatch => { 31 | dispatch({ 32 | type: 'UNBOX', 33 | caseId: caseId, 34 | amount, 35 | }); 36 | 37 | return axios 38 | .post( 39 | `/cases/${caseId}/open`, 40 | { 41 | amount: amount, 42 | }, 43 | { responseType: 'json' } 44 | ) 45 | .then(res => { 46 | return dispatch({ 47 | type: `UNBOX_RECEIVED`, 48 | tradeId: res.data.tradeId, 49 | tradeOfferUrl: res.data.tradeOfferUrl, 50 | }); 51 | }) 52 | .catch(error => { 53 | return dispatch({ 54 | type: `UNBOX_ERROR`, 55 | error, 56 | }); 57 | }); 58 | }; 59 | } 60 | 61 | function getUnboxStatus(tradeId) { 62 | return dispatch => { 63 | dispatch({ type: 'GET_UNBOX_STATUS' }); 64 | 65 | return axios 66 | .get(`/offer/${tradeId}`) 67 | .then(res => { 68 | return dispatch({ 69 | type: `GET_UNBOX_STATUS_RECEIVED`, 70 | items: res.data.items, 71 | offerState: res.data.offerState, 72 | openingState: res.data.openingState, 73 | totalExpectedItems: res.data.totalExpectedItems, 74 | }); 75 | }) 76 | .catch(error => { 77 | return dispatch({ 78 | type: `GET_UNBOX_STATUS_ERROR`, 79 | error, 80 | }); 81 | }); 82 | }; 83 | } 84 | 85 | function getAvailableKeys() { 86 | return dispatch => { 87 | return axios 88 | .get(`/keys`) 89 | .then(res => { 90 | return dispatch({ 91 | type: `GET_AVAILABLE_KEYS_RECEIVED`, 92 | keyCount: res.data.keyCount, 93 | }); 94 | }) 95 | .catch(error => { 96 | return dispatch({ 97 | type: `GET_AVAILABLE_KEYS_ERROR`, 98 | error, 99 | }); 100 | }); 101 | }; 102 | } 103 | 104 | function exit() { 105 | return { 106 | type: `EXIT`, 107 | }; 108 | } 109 | 110 | function endUnboxing() { 111 | return { 112 | type: `END_UNBOXING`, 113 | }; 114 | } 115 | 116 | function getCaseItems(kase) { 117 | return dispatch => { 118 | if (kase.items) { 119 | //Items were already loaded for this case. Nothing to do. 120 | return Promise.resolve(); 121 | } 122 | let skuFilter = kase.skus.join(','); 123 | return axios 124 | .get(`/items?${qs.stringify({ skus: skuFilter })}`) 125 | .then(res => { 126 | return dispatch({ 127 | type: `GET_CASE_ITEMS_RECEIVED`, 128 | caseId: kase.id, 129 | items: res.data.items, 130 | }); 131 | }) 132 | .catch(error => { 133 | return dispatch({ 134 | type: `GET_CASE_ITEMS_ERROR`, 135 | error, 136 | }); 137 | }); 138 | }; 139 | } 140 | 141 | function shuffleCases() { 142 | return { 143 | type: `SHUFFLE_CASES`, 144 | }; 145 | } 146 | 147 | function showNavBar(show) { 148 | return { 149 | type: `SHOW_NAV_BAR`, 150 | showNavBar: show, 151 | }; 152 | } 153 | 154 | function authStatus(authenticated, username, avatar) { 155 | return { 156 | type: 'AUTH_STATUS', 157 | authenticated: authenticated, 158 | username: username, 159 | avatar: avatar, 160 | }; 161 | } 162 | 163 | function getAuthStatus() { 164 | return dispatch => { 165 | return axios 166 | .get('/auth/status') 167 | .then(res => { 168 | storeInStorage('AUTHENTICATION_STATUS', res.data); 169 | return dispatch( 170 | authStatus(res.data.authenticated, res.data.username, res.data.avatar) 171 | ); 172 | }) 173 | .catch(error => { 174 | return dispatch(authStatus(false, '', '')); 175 | }); 176 | }; 177 | } 178 | 179 | function logout() { 180 | return dispatch => { 181 | return axios 182 | .delete('/auth') 183 | .then(res => { 184 | dispatch(exit()); 185 | storeInStorage('AUTHENTICATION_STATUS', { authenticated: false }); 186 | return dispatch({ 187 | type: 'AUTH_STATUS', 188 | status: false, 189 | }); 190 | }) 191 | .catch(error => { 192 | return dispatch({ 193 | type: 'LOGOUT_ERROR', 194 | }); 195 | }); 196 | }; 197 | } 198 | 199 | export { 200 | getCases, 201 | unbox, 202 | getUnboxStatus, 203 | getAvailableKeys, 204 | exit, 205 | endUnboxing, 206 | authStatus, 207 | getCaseItems, 208 | shuffleCases, 209 | logout, 210 | getAuthStatus, 211 | }; 212 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const router = require('koa-router')(); 3 | const _ = require('lodash'); 4 | const Promise = require('bluebird'); 5 | const log = require('./log'); 6 | const koaLogger = require('koa-bunyan'); 7 | const serve = require('koa-static'); 8 | const rewrite = require('koa-rewrite'); 9 | const { 10 | baseURL, 11 | port, 12 | vgoURL, 13 | vgoAPIKey, 14 | affiliateAddress, 15 | sessionKeys, 16 | steamApiKey, 17 | } = require('./config'); 18 | const Axios = require('axios'); 19 | const qs = require('qs'); 20 | const session = require('koa-session'); 21 | const passport = require('koa-passport'); 22 | const SteamStrategy = require('passport-steam'); 23 | 24 | const app = new Koa(); 25 | 26 | app.use(koaLogger(log, { level: 'info' })); 27 | 28 | app.use(rewrite('/login/callback', '/')); 29 | app.use(rewrite('/info', '/')); 30 | app.use(serve('public')); 31 | router.get('/health', healthCheck); 32 | router.get('/cases', getCases); 33 | router.get('/keys', getKeyCount); 34 | router.post('/cases/:id/open', sendCaseOpenOffer); 35 | router.get('/offer/:id', getCaseOpenOfferState); 36 | router.get('/items', getItems); 37 | router.get( 38 | '/auth', 39 | passport.authenticate('steam', { 40 | failureRedirect: '/', 41 | successRedirect: '/login/callback', 42 | }) 43 | ); 44 | router.get( 45 | '/auth/completed', 46 | passport.authenticate('steam', { 47 | failureRedirect: '/', 48 | successRedirect: '/login/callback', 49 | }) 50 | ); 51 | router.get('/auth/status', getAuthStatus); 52 | router.delete('/auth', logout); 53 | 54 | const bodyParser = require('koa-bodyparser'); 55 | app.use(bodyParser()); 56 | 57 | require('koa-qs')(app); 58 | 59 | app.keys = sessionKeys; 60 | app.use(session({}, app)); 61 | 62 | passport.serializeUser(function(user, done) { 63 | done(null, user); 64 | }); 65 | 66 | passport.deserializeUser(function(obj, done) { 67 | done(null, obj); 68 | }); 69 | 70 | app.use(passport.initialize()); 71 | app.use(passport.session()); 72 | 73 | passport.use( 74 | new SteamStrategy( 75 | { 76 | returnURL: `${baseURL}/auth/completed`, 77 | realm: `${baseURL}/`, 78 | apiKey: steamApiKey, 79 | }, 80 | function(identifier, profile, done) { 81 | let user = { 82 | steamId: _.last(identifier.split('/')), 83 | name: profile._json.personaname, 84 | avatar: profile._json.avatar, 85 | }; 86 | done(null, user); 87 | } 88 | ) 89 | ); 90 | 91 | app.use(async (ctx, next) => { 92 | try { 93 | await next(); 94 | } catch (error) { 95 | // 500 handler 96 | log.error('Error handling request', error); 97 | ctx.body = { 98 | error, 99 | }; 100 | ctx.status = 500; 101 | } 102 | }); 103 | 104 | app.use(router.routes()); 105 | 106 | app.listen(port, () => { 107 | log.info(`VCase API listening on: ${port}`); 108 | }); 109 | 110 | const vgoAPI = Axios.create({ 111 | baseURL: vgoURL, 112 | headers: { 113 | Authorization: 'Basic ' + Buffer.from(vgoAPIKey + ':').toString('base64'), 114 | }, 115 | }); 116 | 117 | async function healthCheck(ctx) { 118 | ctx.body = { 119 | api: 'up', 120 | }; 121 | ctx.status = 200; 122 | } 123 | 124 | async function getCases(ctx) { 125 | let response = await vgoAPI.get('/ICase/GetCaseSchema/v1'); 126 | ctx.body = response.data.response.cases; 127 | ctx.status = 200; 128 | } 129 | 130 | async function getKeyCount(ctx) { 131 | if (ctx.isUnauthenticated()) { 132 | ctx.status = 401; 133 | ctx.body = `{"error":"Unauthenticated"}`; 134 | return; 135 | } 136 | let response; 137 | try { 138 | response = await vgoAPI.get( 139 | `/ICaseSite/GetKeyCount/v1?${qs.stringify({ 140 | steam_id: ctx.state.user.steamId, 141 | })}` 142 | ); 143 | ctx.body = { keyCount: response.data.response.key_count }; 144 | ctx.status = 200; 145 | } catch (error) { 146 | ctx.body = { message: error.response.data.message }; 147 | ctx.status = error.response.status; 148 | } 149 | } 150 | 151 | async function sendCaseOpenOffer(ctx) { 152 | if (ctx.isUnauthenticated()) { 153 | ctx.status = 401; 154 | ctx.body = `{"error":"Unauthenticated"}`; 155 | return; 156 | } 157 | 158 | let caseId = ctx.params.id; 159 | let amount = ctx.request.body.amount; 160 | 161 | let response = await vgoAPI.post('/ICaseSite/SendKeyRequest/v1', { 162 | steam_id: ctx.state.user.steamId, 163 | case_id: caseId, 164 | amount: amount, 165 | affiliate_eth_address: affiliateAddress, 166 | }); 167 | ctx.body = { 168 | tradeId: response.data.response.offer.id, 169 | tradeOfferUrl: response.data.response.offer_url, 170 | }; 171 | ctx.status = 200; 172 | } 173 | 174 | async function getCaseOpenOfferState(ctx) { 175 | const OFFER_PENDING = 0; 176 | const OFFER_ACCEPTED = 1; 177 | const OFFER_FAILED = -1; 178 | const OPENING_PENDING = 0; 179 | const OPENING_COMPLETED = 1; 180 | const OPENING_FAILED = -1; 181 | const OPENING_PARTIAL_FAILURE = -2; 182 | 183 | let offerId = ctx.params.id; 184 | let response = await vgoAPI.get( 185 | `/ICaseSite/GetTradeStatus/v1?offer_id=${offerId}` 186 | ); 187 | let items = _.chain(response.data.response.cases) 188 | .map(function(kase) { 189 | if (kase.item === null) { 190 | return null; 191 | } 192 | return { 193 | name: kase.item.name, 194 | category: kase.item.category, 195 | wearTier: kase.item.wear_tier, 196 | image: kase.item.image, 197 | color: kase.item.color, 198 | }; 199 | }) 200 | .compact(); 201 | let offerState; 202 | switch (response.data.response.offer.state) { 203 | case 2: 204 | offerState = OFFER_PENDING; 205 | break; 206 | case 9: 207 | case 11: 208 | case 3: 209 | offerState = OFFER_ACCEPTED; 210 | break; 211 | default: 212 | offerState = OFFER_FAILED; 213 | } 214 | let openingState; 215 | let cases = response.data.response.cases; 216 | let itemsCount = response.data.response.offer.recipient.items.length; 217 | if (cases.length < itemsCount || cases.some(kase => kase.status === 2)) { 218 | openingState = OPENING_PENDING; 219 | } else if (cases.every(kase => kase.status === 3)) { 220 | openingState = OPENING_COMPLETED; 221 | } else if (cases.every(kase => kase.status === 1)) { 222 | openingState = OPENING_FAILED; 223 | } else { 224 | openingState = OPENING_PARTIAL_FAILURE; 225 | } 226 | ctx.body = { 227 | offerState: offerState, 228 | openingState: openingState, 229 | items: items, 230 | totalExpectedItems: itemsCount, 231 | }; 232 | ctx.status = 200; 233 | } 234 | 235 | async function getItems(ctx) { 236 | let skus = ctx.query.skus; 237 | let response = await vgoAPI.get( 238 | `/IItem/GetItems/v1?sku_filter=${skus}&wear_tier_index=1` 239 | ); 240 | let items = _.map(response.data.response.items, function(item, sku) { 241 | let itemInfo = item['1']; 242 | return { 243 | name: itemInfo.name, 244 | category: itemInfo.category, 245 | wearTier: itemInfo.wear_tier, 246 | image: itemInfo.image, 247 | color: itemInfo.color, 248 | sku: sku, 249 | }; 250 | }); 251 | ctx.body = { 252 | items: items, 253 | }; 254 | ctx.status = 200; 255 | } 256 | 257 | async function getAuthStatus(ctx) { 258 | if (ctx.isAuthenticated()) { 259 | ctx.body = { 260 | authenticated: true, 261 | username: ctx.state.user.name, 262 | avatar: ctx.state.user.avatar, 263 | }; 264 | } else { 265 | ctx.body = { 266 | authenticated: false, 267 | username: '', 268 | avatar: '', 269 | }; 270 | } 271 | 272 | ctx.status = 200; 273 | } 274 | 275 | async function logout(ctx) { 276 | await ctx.logout(); 277 | ctx.body = {}; 278 | ctx.status = 200; 279 | } 280 | -------------------------------------------------------------------------------- /vgomock/index.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const router = require('koa-router')(); 3 | const serve = require('koa-static'); 4 | const koaBunyanLogger = require('koa-bunyan-logger'); 5 | const PORT = 3002; 6 | 7 | const app = new Koa(); 8 | require('koa-qs')(app); 9 | app.use(koaBunyanLogger()); 10 | app.use(serve('public')); 11 | 12 | router.get('/ICase/GetCaseSchema/v1', function(ctx) { 13 | ctx.body = { 14 | status: 1, 15 | time: 1525284327, 16 | response: { 17 | cases: [ 18 | { 19 | id: 1, 20 | name: 'VGO Case 1', 21 | image: { '300px': `http://localhost:${PORT}/img/img-01.png` }, 22 | skus: [1, 2, 3, 4, 5, 6], 23 | }, 24 | { 25 | id: 2, 26 | name: 'VGO Case 2', 27 | image: { '300px': `http://localhost:${PORT}/img/img-01.png` }, 28 | skus: [7, 8, 9, 10], 29 | }, 30 | { 31 | id: 3, 32 | name: 'VGO Case 3', 33 | image: { '300px': `http://localhost:${PORT}/img/img-01.png` }, 34 | skus: [7, 8, 9, 10], 35 | }, 36 | { 37 | id: 4, 38 | name: 'VGO Case 4', 39 | image: { '300px': `http://localhost:${PORT}/img/img-01.png` }, 40 | skus: [7, 8, 9, 10], 41 | }, 42 | { 43 | id: 5, 44 | name: 'VGO Case 5', 45 | image: { '300px': `http://localhost:${PORT}/img/img-01.png` }, 46 | skus: [7, 8, 9, 10], 47 | }, 48 | ], 49 | }, 50 | }; 51 | }); 52 | 53 | router.get('/ICaseSite/GetKeyCount/v1', function(ctx) { 54 | if (!ctx.query.steam_id) { 55 | ctx.status = 400; 56 | ctx.body = { error: "Missing 'steam_id'" }; 57 | return; 58 | } 59 | console.log('STEAM ID: ' + ctx.query.steam_id); 60 | ctx.body = { 61 | status: 1, 62 | time: 1525284327, 63 | response: { 64 | key_count: 15, 65 | }, 66 | }; 67 | }); 68 | 69 | router.post('/ICaseSite/SendKeyRequest/v1', function(ctx) { 70 | if (!ctx.request.body.steam_id) { 71 | ctx.status = 400; 72 | ctx.body = { error: "Missing 'steam_id'" }; 73 | return; 74 | } 75 | if (!ctx.request.body.case_id) { 76 | ctx.status = 400; 77 | ctx.body = { error: "Missing 'case_id'" }; 78 | return; 79 | } 80 | if (!ctx.request.body.affiliate_eth_address) { 81 | ctx.status = 400; 82 | ctx.body = { error: "Missing 'affiliate_eth_address'" }; 83 | return; 84 | } 85 | 86 | ctx.body = { 87 | status: 1, 88 | time: 1525284327, 89 | response: { 90 | offer: { 91 | id: 1000, 92 | sender: { 93 | uid: 1000, 94 | items: [], 95 | }, 96 | recipient: { 97 | uid: 1000, 98 | items: [ 99 | { 100 | wear_tier: 'Factory New', 101 | name: 'Key', 102 | category: 'Classified SMG', 103 | color: '#8847ff', 104 | image: { 105 | '300px': 'http://localhost:${PORT}/img/img-02.png', 106 | '600px': 'http://localhost:${PORT}/img/img-03.png', 107 | }, 108 | suggested_price: 5, 109 | }, 110 | ], 111 | }, 112 | state: 2, 113 | state_name: 'active', 114 | time_created: 1525879295, 115 | time_updated: 1525879295, 116 | time_expires: 1526139117, 117 | message: '', 118 | sent_by_you: true, 119 | }, 120 | offer_url: `http://localhost:${PORT}/trade_offer.html`, 121 | }, 122 | }; 123 | }); 124 | 125 | var tradeStatusRequestCount = 0; 126 | router.get('/ICaseSite/GetTradeStatus/v1', function(ctx) { 127 | tradeStatusRequestCount = (tradeStatusRequestCount + 1) % 10; 128 | 129 | if (!ctx.query.offer_id) { 130 | ctx.status = 400; 131 | ctx.body = { error: "Missing 'offer_id'" }; 132 | return; 133 | } 134 | let offerState = 2; 135 | if (tradeStatusRequestCount > 1) { 136 | offerState = 9; 137 | } else if (tradeStatusRequestCount > 4) { 138 | offerState = 11; 139 | } else if (tradeStatusRequestCount > 8) { 140 | offerState = 3; 141 | } 142 | ctx.body = { 143 | status: 1, 144 | time: 1525284327, 145 | response: { 146 | offer: { 147 | id: 1000, 148 | sender: { 149 | uid: 1000, 150 | items: [], 151 | }, 152 | recipient: { 153 | uid: 1000, 154 | items: [ 155 | { 156 | category: 'VGO Key', 157 | color: '#777777', 158 | eth_inspect: null, 159 | id: 25229, 160 | image: { 161 | '300px': 'https://img-staging.vgo.gg/item/skeleton-key-300.png', 162 | '600px': 'https://img-staging.vgo.gg/item/skeleton-key-600.png', 163 | }, 164 | missing: true, 165 | name: 'Skeleton Key', 166 | pattern_index: null, 167 | preview_urls: null, 168 | sku: 1, 169 | suggested_price: 250, 170 | wear: null, 171 | wear_tier: '', 172 | }, 173 | { 174 | category: 'VGO Key', 175 | color: '#777777', 176 | eth_inspect: null, 177 | id: 25229, 178 | image: { 179 | '300px': 'https://img-staging.vgo.gg/item/skeleton-key-300.png', 180 | '600px': 'https://img-staging.vgo.gg/item/skeleton-key-600.png', 181 | }, 182 | missing: true, 183 | name: 'Skeleton Key', 184 | pattern_index: null, 185 | preview_urls: null, 186 | sku: 1, 187 | suggested_price: 250, 188 | wear: null, 189 | wear_tier: '', 190 | }, 191 | ], 192 | }, 193 | state: offerState, 194 | state_name: 'active', 195 | time_created: 1525879295, 196 | time_updated: 1525879295, 197 | time_expires: 1526139117, 198 | message: '', 199 | }, 200 | cases: [ 201 | { 202 | id: 1, 203 | status: tradeStatusRequestCount > 4 ? 3 : 2, 204 | status_text: tradeStatusRequestCount < 6 ? 'Pending' : 'Opened', 205 | case_id: 1, 206 | case_site_trade_offer_id: null, 207 | item: { 208 | wear_tier: 'Factory New', 209 | name: 'UMP-45 | Primal Saber', 210 | category: 'Classified SMG', 211 | color: '#8847ff', 212 | image: { 213 | '300px': `http://localhost:${PORT}/img/img-02.png`, 214 | '600px': `http://localhost:${PORT}/img/img-03.png`, 215 | }, 216 | suggested_price: 5, 217 | }, 218 | }, 219 | { 220 | id: 1, 221 | status: tradeStatusRequestCount > 8 ? 3 : 2, 222 | status_text: tradeStatusRequestCount < 9 ? 'Pending' : 'Opened', 223 | case_id: 1, 224 | case_site_trade_offer_id: null, 225 | item: { 226 | wear_tier: 'Factory New', 227 | name: 'AK-47 | Case Hardened', 228 | category: 'Classified Rifle', 229 | color: '#8847ff', 230 | image: { 231 | '300px': `http://localhost:${PORT}/img/img-02.png`, 232 | '600px': `http://localhost:${PORT}/img/img-03.png`, 233 | }, 234 | suggested_price: 5, 235 | }, 236 | }, 237 | ], 238 | }, 239 | }; 240 | }); 241 | 242 | router.get('/IItem/GetItems/v1', function(ctx) { 243 | if (!ctx.query.sku_filter) { 244 | ctx.status = 400; 245 | ctx.body = { error: "Missing 'sku_filter'" }; 246 | return; 247 | } 248 | if (!ctx.query.wear_tier_index) { 249 | ctx.status = 400; 250 | ctx.body = { error: "Missing 'wear_tier_index'" }; 251 | return; 252 | } 253 | 254 | ctx.body = { 255 | status: 1, 256 | time: 1524850074, 257 | response: { 258 | items: { 259 | '10': { 260 | '1': { 261 | wear_tier: 'Factory New', 262 | name: 'UMP-45 | Primal Saber', 263 | category: 'Classified SMG', 264 | color: '#8847ff', 265 | image: { 266 | '300px': `http://localhost:${PORT}/img/img-02.png`, 267 | '600px': `http://localhost:${PORT}/img/img-03.png`, 268 | }, 269 | suggested_price: 5, 270 | }, 271 | }, 272 | '20': { 273 | '1': { 274 | wear_tier: 'Factory New', 275 | name: 'AK-47 | Case Hardened', 276 | category: 'Classified Rifle', 277 | color: '#8847ff', 278 | image: { 279 | '300px': `http://localhost:${PORT}/img/img-02.png`, 280 | '600px': `http://localhost:${PORT}/img/img-03.png`, 281 | }, 282 | suggested_price: 1099, 283 | }, 284 | }, 285 | }, 286 | }, 287 | }; 288 | }); 289 | 290 | const bodyParser = require('koa-bodyparser'); 291 | app.use(bodyParser()); 292 | 293 | app.use(async (ctx, next) => { 294 | try { 295 | await next(); 296 | } catch (error) { 297 | // 500 handler 298 | ctx.log.error('Error handling request', error); 299 | ctx.body = { 300 | error, 301 | }; 302 | ctx.status = 500; 303 | } 304 | }); 305 | 306 | app.use(router.routes()); 307 | 308 | app.listen(PORT, () => { 309 | console.log(`VOG MOCK API listening on: ${PORT}`); 310 | }); 311 | -------------------------------------------------------------------------------- /client/src/containers/faq/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import $ from 'jquery'; 3 | 4 | class FaqList extends Component { 5 | componentDidMount() { 6 | $('#accordion').on('show.bs.collapse', e => { 7 | $(e.target) 8 | .parent() 9 | .addClass('faq-shown'); 10 | }); 11 | $('#accordion').on('hide.bs.collapse', e => { 12 | $(e.target) 13 | .parent() 14 | .removeClass('faq-shown'); 15 | }); 16 | } 17 | 18 | componentWillUnmount() { 19 | $('#accordion').off('show.bs.collapse'); 20 | $('#accordion').off('hide.bs.collapse'); 21 | } 22 | 23 | render() { 24 | let faqIndex = 0; 25 | return ( 26 |
    27 |
    28 |
    29 |
    30 |

    VCASE FAQ

    31 |
    32 |
    33 |
    34 |
    35 | 44 |
    45 |
    46 |
    52 |
    53 | vCase.gg is the case opening site for{' '} 54 | 55 | VGO items.{' '} 56 | . vCases are opened with a vKey. 57 |
    58 |
    59 | VGO items are digital items generated using blockchain 60 | technology, so anyone can trade a VGO item to whoever they 61 | want, whenever they want, without any restrictions, trade 62 | holds, or fear of bannings. Every VGO item is 63 | one-of-a-kind and generated from an Ethereum smart 64 | contract. These items cannot ever be subjected to any 65 | trading restrictions such as trade holds or bans. 66 |
    67 |
    68 |
    69 |
    70 |
    71 |
    72 | 81 |
    82 |
    83 |
    89 |
    90 | There are two ways to get a vKey: 91 |
      92 |
    • 93 | Purchase one from a marketplace that supports VGO 94 | items 95 |
    • 96 |
    • Get a vKey in a trade from another VGO user
    • 97 |
    98 |
    99 |
    100 |
    101 |
    102 |
    103 |
    104 | 113 |
    114 |
    115 |
    121 |
    122 | For simplicity, any vKey can open any vCase. 123 |
    124 |
    125 |
    126 |
    127 |
    128 |
    129 | 138 |
    139 |
    140 |
    146 |
    147 | vCase.gg is based on the Ethereum blockchain for now, 148 | which brings many advantages over traditional case opening 149 | sites, mainly transparency. However until we migrate to 150 | the WAX Blockchain, case openings can take up to two 151 | minutes to complete because of the speed of Ethereum 152 | blockchain. The WAX Blockchain will be much faster and 153 | will make case openings instant. 154 |
    155 |
    156 |
    157 |
    158 |
    159 |
    160 | 169 |
    170 |
    171 |
    177 |
    178 | The VGO items that you receive from a vCase opening are 179 | sent to your{' '} 180 | 181 | OPSkins ExpressTrade Inventory 182 | . 183 |
    184 |
    185 |
    186 |
    187 |
    188 |
    189 | 198 |
    199 |
    200 |
    206 |
    207 | You can trade your VGO items to another VGO user for free 208 | on VGO.gg, as long as you have their VGO Trade URL. You 209 | can also sell or trade them to any marketplace that 210 | supports VGO items. 211 |
    212 |
    213 |
    214 |
    215 |
    216 |
    217 | 226 |
    227 |
    228 |
    234 |
    235 | VCases's opening odds are on average around 3x better than 236 | Steam's case opening odds. 237 |
    238 |
    239 |
    240 |
    241 |
    242 |
    243 | 253 |
    254 |
    255 |
    261 |
    262 | You can check the results of all vCase openings because 263 | all items generated from vCases are recorded on the 264 | Ethereum blockchain. Each item generated links back to a 265 | blockchain transaction for full transparency. For this 266 | reason, VGO items also cannot be duplicated. 267 |
    268 |
    269 |
    270 |
    271 |
    272 |
    273 | 283 |
    284 |
    285 |
    291 |
    292 | No. Since VGO uses blockchain technology, neither Steam or 293 | anyone else can shut it down. If the OPSkins ExpressTrade 294 | website disappeared tomorrow, the data behind the items 295 | would still exist and could be rendered with graphics to 296 | showcase the unique properties of the items. 297 |
    298 |
    299 |
    300 |
    301 |
    302 |
    303 | 312 |
    313 |
    314 |
    320 |
    321 | Because vCase is operating on the Ethereum blockchain at 322 | this time, there is a minimum case opening quantity due to 323 | ETH gas fees. Once we migrate to the WAX Blockchain, these 324 | minimums will change 325 |
    326 |
    327 |
    328 |

    329 | How can I build my own vCase opening site? 330 |

    331 |
    332 |
    333 |
    334 | 343 |
    344 |
    345 |
    351 |
    352 | Yes. The vCase code is open source and available on our{' '} 353 | 354 | GitHub 355 | . 356 |
    357 |
    358 |
    359 |
    360 |
    361 |
    362 | 371 |
    372 |
    373 |
    379 |
    380 | Yes. All sites that adopt the vCase functionality will 381 | automatically earn a 5% affiliate fee, paid to you in 382 | real-time in ETH. Because the vCase affiliate program is 383 | smart-contract based, when someone uses a vKey to open a 384 | vCase on your site, your payment will be sent instantly to 385 | your Ethereum address. 386 |
    387 |
    388 |
    389 |
    390 |
    391 |
    392 |
    393 |
    394 | ); 395 | } 396 | } 397 | 398 | export default FaqList; 399 | -------------------------------------------------------------------------------- /client/src/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | min-height: 100%; 3 | position: relative; 4 | } 5 | 6 | body { 7 | font-family: 'Lato', 'Helvetica Neue', Helvetica, Arial, sans-serif; 8 | /* max-width: 1920px !important; 9 | margin: 0 auto; */ 10 | background: #2d2f39; 11 | margin-bottom: 120px; /*Footer fixed height*/ 12 | } 13 | 14 | input, button, a { 15 | border-radius: 0px !important; 16 | } 17 | 18 | h1, 19 | h2, 20 | h3, 21 | h4, 22 | h5, 23 | h6 { 24 | font-family: 'Lato', 'Helvetica Neue', Helvetica, Arial, sans-serif; 25 | font-weight: 700; 26 | } 27 | 28 | .txt-blue {color: #4f98cf;} 29 | .txt-white {color: #fff;} 30 | .txt-gray {color: #999;} 31 | 32 | nav{ 33 | background: rgba(0,0,0,0.45); 34 | height: 80px; 35 | visibility: hidden; 36 | } 37 | @media (max-width: 991px) { 38 | nav { 39 | background: rgba(0,0,0,0.45) !important; 40 | } 41 | } 42 | 43 | nav.hide-bg-and-logo { 44 | background: transparent; 45 | visibility: visible; 46 | } 47 | nav.hide-bg-and-logo.visible { 48 | background: rgba(0,0,0,0.45) !important; 49 | } 50 | 51 | nav.hide-bg-and-logo .navbar-brand { 52 | visibility: hidden; 53 | } 54 | nav.hide-bg-and-logo.visible .navbar-brand { 55 | visibility: visible; 56 | } 57 | 58 | a.btn-primary { 59 | background-color: #4f98cf; 60 | } 61 | 62 | nav button.btn-primary { 63 | color: white; 64 | background-color: #4f98cf; 65 | } 66 | nav a.open-case-btn-header { 67 | color: #4f98cf; 68 | font-size: 1.1rem; 69 | font-weight: 600; 70 | } 71 | nav a.open-case-btn-header:hover { 72 | text-decoration: none; 73 | } 74 | nav .navbar-login { 75 | right: 20px; 76 | position: absolute; 77 | display: flex; 78 | align-items: center; 79 | } 80 | nav .navbar-login .login-btn img { 81 | height: 24px; 82 | } 83 | nav .nav-divider { 84 | border-right: 1px solid #444; 85 | margin: 0 30px; 86 | align-self: stretch; 87 | } 88 | 89 | nav .navbar-login .nav-user { 90 | display: flex; 91 | } 92 | nav .navbar-login .nav-user .dropdown.nav-item { 93 | display: flex; 94 | } 95 | nav .navbar-login .nav-user .dropdown.nav-item > .dropdown-toggle { 96 | color: #fff; 97 | padding: 15px 0; 98 | } 99 | 100 | nav .navbar-login .nav-user .dropdown.nav-item .nav-user-avatar { 101 | width: 50px; 102 | height: auto; 103 | border-radius: 50%; 104 | border: 2px solid #fff; 105 | margin-right: 15px; 106 | vertical-align: bottom; 107 | } 108 | nav .navbar-login .nav-user .dropdown.nav-item .nav-username { 109 | font-size: 1.2rem; 110 | } 111 | nav .navbar-login .nav-user .dropdown.nav-item .dropdown-menu:before { 112 | content:''; 113 | display:block; 114 | width:0; 115 | height:0; 116 | position:absolute; 117 | 118 | border-right: 11px solid transparent; 119 | border-left: 11px solid transparent; 120 | border-bottom:11px solid rgba(77, 152, 207, 0.5); 121 | top:-11px; 122 | 123 | right:15px; 124 | } 125 | nav .navbar-login .nav-user .dropdown.nav-item .dropdown-menu { 126 | border-radius: 0; 127 | background-color: rgba(77, 152, 207, 0.5); 128 | margin-top: 0; 129 | } 130 | nav .navbar-login .nav-user .dropdown.nav-item .dropdown-menu .dropdown-item { 131 | color: #fff; 132 | font-size: 1.1rem; 133 | text-align: right; 134 | } 135 | nav .navbar-login .nav-user .dropdown.nav-item .dropdown-menu .dropdown-item:hover { 136 | background-color: rgba(77, 152, 207, 0.9); 137 | } 138 | nav .navbar-login .nav-user .dropdown.nav-item .dropdown-menu .dropdown-item:active { 139 | background-color: rgba(77, 152, 207, 0.2); 140 | } 141 | 142 | .btn-secondary { 143 | color: #4f98cf; 144 | background-color: white; 145 | margin-left: 12px 146 | } 147 | 148 | .navbar { 149 | z-index: 6; 150 | } 151 | 152 | .navbar-brand { 153 | padding-top: 0; 154 | padding-bottom: 0; 155 | position: absolute; 156 | z-index: 1; 157 | top: 0px; 158 | left: 20px; 159 | } 160 | 161 | header.masthead { 162 | position: relative; 163 | background: url("/img/back-header.png") no-repeat center center #2d2f39; 164 | -webkit-background-size: cover; 165 | -moz-background-size: cover; 166 | -o-background-size: cover; 167 | background-size: cover; 168 | padding-top: 30rem; 169 | padding-bottom: 22rem; 170 | z-index: 5; 171 | } 172 | .chev-container { 173 | position: relative; 174 | width: 100%; 175 | z-index: 5; 176 | } 177 | /* header design element */ 178 | .chev-container .left { 179 | position: absolute; 180 | height: 5.5em; 181 | width: 50%; 182 | left: 0; 183 | bottom: 3em; 184 | background: #4f98cf; 185 | transform: skewY(8deg); 186 | } 187 | .chev-container .right { 188 | position: absolute; 189 | height: 5.5em; 190 | width: 50%; 191 | right: 0; 192 | bottom: 3em; 193 | background: #4f98cf; 194 | transform: skewY(-8deg); 195 | } 196 | .chev-container .left2 { 197 | position: absolute; 198 | height: 9.5em; 199 | width: 50%; 200 | left: 0; 201 | bottom: -2em; 202 | background: #2d2f39; 203 | transform: skewY(9deg); 204 | } 205 | .chev-container .right2 { 206 | position: absolute; 207 | height: 9.5em; 208 | width: 50%; 209 | right: 0; 210 | bottom: -2em; 211 | background: #2d2f39; 212 | transform: skewY(-9deg); 213 | } 214 | /* end header design element */ 215 | /* footer version */ 216 | .divisor .chev-container .left { 217 | bottom: -2em; 218 | } 219 | .divisor .chev-container .right { 220 | bottom: -2em; 221 | } 222 | .divisor .chev-container .left2 { 223 | bottom: -1em; 224 | transform: skewY(6deg); 225 | } 226 | .divisor .chev-container .right2 { 227 | bottom: -1em; 228 | transform: skewY(-6deg); 229 | } 230 | /* end footer version */ 231 | 232 | header.masthead .overlay { 233 | position: absolute; 234 | background-color: #212529; 235 | height: 100%; 236 | width: 100%; 237 | top: 0; 238 | left: 0; 239 | opacity: 0.3; 240 | } 241 | 242 | header.masthead h1 { 243 | font-size: 2rem; 244 | } 245 | 246 | header.masthead input { 247 | background: #262626; 248 | color: white; 249 | border: 4px solid rgba(255,255,255,.3); 250 | margin-top: 2em; 251 | } 252 | 253 | header.masthead button { 254 | background: #4f98cf; 255 | color: white; 256 | text-transform: uppercase; 257 | margin: 0; 258 | } 259 | 260 | header.masthead .but-header { 261 | margin: 0 auto 30px; 262 | } 263 | 264 | .developers{ 265 | margin-top: -13em; 266 | background: white; 267 | } 268 | 269 | 270 | .developers .row{ 271 | padding: 12rem 0 8rem; 272 | } 273 | 274 | .developers .title { 275 | margin-top: 45px; 276 | } 277 | 278 | .developers .title h2 { 279 | text-transform: uppercase; 280 | } 281 | 282 | .developers .button{ 283 | margin-top: 70px; 284 | } 285 | 286 | .developers .button a{ 287 | width: 200px; 288 | background: #4f98cf; 289 | } 290 | 291 | .divisor{ 292 | height: 15.5em; 293 | z-index: 2; 294 | position: relative; 295 | top: 6em; 296 | margin-top: -6em; 297 | } 298 | 299 | .faq{ 300 | background: url("/img/back-QA.png") no-repeat center 5em transparent; 301 | padding: 50px 0 160px 0; 302 | z-index: 5; 303 | position: relative; 304 | font-weight: 600; 305 | } 306 | 307 | .faq h2 { 308 | color: #4f98cf; 309 | text-align: center; 310 | padding-bottom: 50px; 311 | } 312 | 313 | .faq .sub-header { 314 | padding-bottom: 20px; 315 | padding-top: 5px; 316 | } 317 | 318 | .faq .card { 319 | border-radius: 0.25rem; 320 | overflow: hidden; 321 | background-color: transparent; 322 | margin-bottom: 15px; 323 | } 324 | 325 | .faq .btn-link { 326 | font-weight: 600; 327 | color: #fff; 328 | padding: 0px; 329 | width: 100%; 330 | white-space: normal; 331 | text-align: left; 332 | line-height: 1.5rem; 333 | } 334 | 335 | .faq .card-body { 336 | background-color: #3f4250; 337 | color: #ccc; 338 | padding-top: 0; 339 | } 340 | 341 | .faq .card-header { 342 | padding: .75rem 1.25rem; 343 | margin-bottom: 0; 344 | background-color: #3f4250; 345 | border-bottom: 0; 346 | } 347 | 348 | .faq .card-body:before { 349 | content: ""; 350 | display: block; 351 | width: 100%; 352 | border-top: 1px solid #555; 353 | padding-bottom: 15px; 354 | } 355 | 356 | .faq .card.faq-shown { 357 | background-color: #4f98cf; 358 | } 359 | 360 | .faq .card .card-header h5 .btn-link:after { 361 | content: "\f054"; 362 | float: right; 363 | font-family: fontawesome; 364 | color: #4f98cf; 365 | font-weight: 500; 366 | font-size: 1.5em; 367 | } 368 | 369 | .faq .card.faq-shown .card-header h5 .btn-link:after { 370 | content: "\f078"; 371 | } 372 | 373 | .faq .opening-odds-table { 374 | margin: 15px 0; 375 | } 376 | .faq .opening-odds-table th, .faq .opening-odds-table td { 377 | padding: 2px 5px; 378 | } 379 | .faq .opening-odds-table th:last-child, .faq .opening-odds-table td:last-child { 380 | text-align: right; 381 | } 382 | .faq .opening-odds-table thead tr { 383 | background-color: #222; 384 | } 385 | .faq .opening-odds-table tbody tr:nth-child(even) { 386 | background-color: #222; 387 | } 388 | 389 | footer.footer { 390 | padding-top: 0; 391 | padding-bottom: 20px; 392 | background: #1d1f25; 393 | text-align: center; 394 | position: absolute; 395 | bottom: 0; 396 | height: 120px; /*Footer fixed height*/ 397 | width: 100%; 398 | } 399 | 400 | footer.footer img { 401 | margin-top: -45px; 402 | } 403 | 404 | .opacity { 405 | background: rgba(0,0,0,.8); 406 | position: fixed; 407 | width: 100%; 408 | height: 100%; 409 | top: 0; 410 | left: 0; 411 | z-index: 10; 412 | } 413 | 414 | .modal.confirm p{ 415 | color: white; 416 | } 417 | 418 | .modal.confirm .modal-header { 419 | border: 0px; 420 | padding: 0; 421 | } 422 | 423 | .modal.confirm{ 424 | text-align: center; 425 | } 426 | 427 | .item-select { 428 | width: 48%; 429 | margin: 1%; 430 | float: left; 431 | background: #1e2027; 432 | } 433 | 434 | .modal.item .container{ 435 | margin-bottom: 30px; 436 | display: table; 437 | } 438 | 439 | .modal.item h5{ 440 | margin-bottom: 20px; 441 | text-align: center !important; 442 | } 443 | .modal.item .btn { 444 | display: table; 445 | margin: 0px auto 30px auto; 446 | clear: both; 447 | } 448 | 449 | .modal.item img{ 450 | width: 80px; 451 | float: left; 452 | background: #121316; 453 | padding: 22px 12px; 454 | } 455 | .modal.item .data{ 456 | width: auto; 457 | float: left; 458 | padding: 8px; 459 | } 460 | .modal.item .data p{ 461 | margin: 0px; 462 | } 463 | 464 | 465 | .modal { 466 | display: block !important; 467 | opacity: 1 !important; 468 | top: 150px; 469 | } 470 | .modal .modal-dialog{ 471 | height: 0px; 472 | } 473 | 474 | .modal .modal-content{ 475 | background: #2d2f39; 476 | border-radius: 0px; 477 | padding: 20px; 478 | } 479 | 480 | .modal .modal-header{ 481 | border: 0px; 482 | } 483 | 484 | .modal .modal-content h5{ 485 | color: white; 486 | } 487 | .modal .modal-content button.btn-outline-secondary{ 488 | background: #4f98cf; 489 | color: white; 490 | } 491 | 492 | .modal .modal-content input{ 493 | background: #262626; 494 | border: 0px; 495 | } 496 | 497 | .modal .input-group { 498 | border: 4px solid rgba(255,255,255,.3); 499 | } 500 | 501 | 502 | .modal .close{ 503 | color: white; 504 | } 505 | 506 | .selection{ 507 | background: #21222a; 508 | padding: 80px 0; 509 | } 510 | 511 | .selection .card{ 512 | background: #2d2f39; 513 | border: 0px; 514 | padding: 15px; 515 | border-radius: 0; 516 | } 517 | 518 | .selection .card .card-image-container p { 519 | padding: 0; 520 | margin: 0; 521 | min-height: 3em; 522 | line-height: 1.5em; 523 | font-weight: 600; 524 | font-size: 0.8em; 525 | color: #fff; 526 | text-align: center; 527 | } 528 | 529 | .selection .card button{ 530 | background: #4f98cf; 531 | color: white; 532 | font-weight: 600; 533 | font-size: 1.1em; 534 | } 535 | 536 | .selection .number-spinner{ 537 | margin: 0 auto; 538 | } 539 | 540 | .selection .number-spinner button{ 541 | background: none; 542 | color: #4f98cf; 543 | border: 0px; 544 | font-size: 1.5em; 545 | } 546 | 547 | .selection .number-spinner input{ 548 | border: 0px; 549 | background: #262626; 550 | color: #4f98cf; 551 | border: 1px solid #4f98cf; 552 | } 553 | 554 | .header-selection { 555 | margin-bottom: 20px; 556 | } 557 | 558 | div.tooltip-inner { 559 | text-align: left; 560 | min-width: 300px; 561 | background-color: #505050; 562 | } 563 | 564 | .header-selection .number{ 565 | background: #4f98cf; 566 | color: #fff; 567 | float: left; 568 | font-size: 2em; 569 | padding: 15px 30px; 570 | height: 100%; 571 | } 572 | 573 | .header-selection .title{ 574 | background: #2d2f39; 575 | color: #fff; 576 | float: left; 577 | padding: 15px 30px; 578 | height: 100%; 579 | } 580 | 581 | .header-selection button{ 582 | color: #4f98cf; 583 | background: white; 584 | } 585 | 586 | .header-selection .title h1{ 587 | font-size: 1.4em !important; margin: 0px; 588 | } 589 | 590 | .header-selection .title small{ 591 | font-size: 0.9em !important; 592 | } 593 | 594 | .header-selection .buy-more{ 595 | padding: 15px; 596 | height: 100%; 597 | text-align: center; 598 | } 599 | 600 | @media (min-width: 768px) { 601 | .header-selection .buy-more { 602 | text-align: left; 603 | } 604 | } 605 | 606 | .header-selection .buy-more a{ 607 | color: #4f98cf; 608 | background: #2d2f39; 609 | font-size: 0.9em !important; 610 | padding: 8px; 611 | } 612 | 613 | .footer-selection { 614 | border: 0px; 615 | background: #3f4250; 616 | color: #fff; 617 | padding: 10px; 618 | margin: 15px 0; 619 | flex-flow: row nowrap; 620 | align-items: center; 621 | font-weight: 600; 622 | } 623 | 624 | .footer-selection p { 625 | flex-grow: 0; 626 | flex-shrink: 0; 627 | border-right: 1px solid #666; 628 | text-align: left; 629 | padding-right: 15px; 630 | margin-right: 15px; 631 | margin-bottom: 0; 632 | line-height: 2rem; 633 | } 634 | 635 | .footer-selection .autopick-controls { 636 | flex-grow: 1; 637 | flex-shrink: 0; 638 | display: flex; 639 | flex-flow: row nowrap; 640 | justify-content: flex-start; 641 | align-items: center; 642 | } 643 | 644 | .footer-selection .autopick-controls small { 645 | line-height: 2rem; 646 | font-weight: 600; 647 | } 648 | 649 | .footer-selection .autopick-controls .number-spinner { 650 | width: 170px; 651 | margin: 0; 652 | height: 2rem; 653 | } 654 | 655 | .footer-selection .autopick-controls button.auto-pick-btn { 656 | background: #4f98cf; 657 | color: white; 658 | margin-left: auto; 659 | } 660 | 661 | .footer-selection .number-spinner button{ 662 | padding: 0 10px; 663 | } 664 | 665 | .opening{ 666 | background: #21222a; 667 | padding: 80px 0; 668 | } 669 | 670 | .opening .card{ 671 | background: #2d2f39; 672 | border: 0px; 673 | padding: 0px; 674 | font-weight: 600; 675 | border-radius: 0; 676 | } 677 | 678 | .opening a{ 679 | text-decoration: none; 680 | } 681 | 682 | .opening .card .data{ 683 | background: #14161a; 684 | padding: 8px; 685 | } 686 | 687 | .opening .card .data p{ 688 | margin: 0px; 689 | } 690 | 691 | .opening .hidden { 692 | visibility: hidden; 693 | } 694 | 695 | .opening .hidden { 696 | visibility: hidden; 697 | } 698 | 699 | .opening .fadein { 700 | -webkit-animation: fadein 2s; /* Safari, Chrome and Opera > 12.1 */ 701 | -moz-animation: fadein 2s; /* Firefox < 16 */ 702 | -ms-animation: fadein 2s; /* Internet Explorer */ 703 | -o-animation: fadein 2s; /* Opera < 12.1 */ 704 | animation: fadein 2s; 705 | } 706 | 707 | @keyframes fadein { 708 | from { opacity: 0; } 709 | to { opacity: 1; } 710 | } 711 | 712 | /* Firefox < 16 */ 713 | @-moz-keyframes fadein { 714 | from { opacity: 0; } 715 | to { opacity: 1; } 716 | } 717 | 718 | /* Safari, Chrome and Opera > 12.1 */ 719 | @-webkit-keyframes fadein { 720 | from { opacity: 0; } 721 | to { opacity: 1; } 722 | } 723 | 724 | /* Internet Explorer */ 725 | @-ms-keyframes fadein { 726 | from { opacity: 0; } 727 | to { opacity: 1; } 728 | } 729 | 730 | /* Opera < 12.1 */ 731 | @-o-keyframes fadein { 732 | from { opacity: 0; } 733 | to { opacity: 1; } 734 | } 735 | 736 | .opening .centered { 737 | text-align: center; 738 | } 739 | 740 | .opening .btn-primary{ 741 | margin-top: 5px; 742 | background-color: #4f98cf; 743 | font-size: 1.2rem; 744 | } 745 | 746 | .opening h5 { 747 | text-align: center; 748 | } 749 | 750 | @media (max-width: 991px) { 751 | .no-pad .modal-content{padding: 0 !important;} 752 | .modal-body {padding: 0;} 753 | .modal {top: 80px;} 754 | .item-select { 755 | width: 100%; 756 | margin: 10px 0; 757 | float: none; 758 | display: table; 759 | clear: both; 760 | } 761 | .text-right {text-align: center !important;} 762 | .text-left {text-align: center !important;} 763 | .developers img { display: none;} 764 | .developers .container {background-position: center center !important;} 765 | .carousel-item img{ max-width: 300px;} 766 | 767 | .header-selection .number { 768 | text-align: center; 769 | width: 50%; 770 | } 771 | .header-selection .title { 772 | text-align: center; 773 | width: 50%; 774 | } 775 | .faq h2 { 776 | padding: 40px 0; 777 | } 778 | } 779 | 780 | @media (max-width: 767px) { 781 | .opening .card .card-image-container .card-img-top{ 782 | width: 100px; 783 | padding: 12px 2px; 784 | } 785 | .opening .card .card-image-container{ 786 | background: #14161a; 787 | flex-grow: 0; 788 | flex-shrink: 0; 789 | display: flex; 790 | align-items: center; 791 | } 792 | 793 | .opening .card{ 794 | width: 100%; 795 | padding: 1px; 796 | background: #2d2f39; 797 | display: flex; 798 | flex-flow: row nowrap; 799 | font-weight: 600; 800 | } 801 | 802 | .opening .card .data{ 803 | width: auto; 804 | flex-grow: 0; 805 | padding: 8px; 806 | background-color: transparent; 807 | } 808 | 809 | .opening .card .data p{ 810 | margin: 0px; 811 | } 812 | 813 | .selection .card .card-img-top { 814 | width: 100%; 815 | padding: 22px 0; 816 | } 817 | 818 | .selection .card { 819 | background: #14161a; 820 | display: flex; 821 | flex-flow: row nowrap; 822 | } 823 | 824 | .selection .card .card-image-container { 825 | background: #14161a; 826 | flex-grow: 0; 827 | flex-shrink: 0; 828 | width: 40%; 829 | } 830 | .selection .card .card-image-container p { 831 | font-size: 1em; 832 | } 833 | 834 | .selection .card .card-body{ 835 | top: 80px; 836 | padding: 15px 5px; 837 | margin-left: 15px; 838 | display: flex; 839 | flex-flow: column nowrap; 840 | justify-content: center; 841 | } 842 | 843 | .selection .card .input-group, .selection .card .card-body{ 844 | width: 100%; 845 | padding: 8px; 846 | } 847 | .footer-selection p { 848 | border-right: 0; 849 | margin-right: 0; 850 | width: 50%; 851 | text-align: center; 852 | padding: 15px; 853 | font-size: 1.2rem; 854 | } 855 | .footer-selection .autopick-controls { 856 | width: 50%; 857 | flex-flow: column nowrap; 858 | padding: 0 15px; 859 | } 860 | .footer-selection .autopick-controls .number-spinner { 861 | width: auto; 862 | } 863 | .footer-selection .autopick-controls button.auto-pick-btn { 864 | margin-left: 0; 865 | margin-top: 15px; 866 | margin-bottom: 15px; 867 | width: 100%; 868 | } 869 | } 870 | .selection .card .input-group { 871 | padding: 8px 0; 872 | } 873 | .selection .card .input-group p{ 874 | margin: 0px; 875 | } 876 | 877 | .selection .number-spinner button { 878 | font-size: 1em; 879 | } 880 | .selection .card .card-body { 881 | padding: 0; 882 | } 883 | .selection .shuffle-cases { 884 | padding: 15px; 885 | } 886 | 887 | @media (max-width: 380px) { 888 | .modal.item .data {font-size: 12px;} 889 | .opening .card .data p{font-size: 12px;} 890 | } 891 | 892 | .waiting { 893 | color: white; 894 | text-align: center; 895 | } 896 | 897 | .waiting-container { 898 | width: 50%; 899 | height: 50%; 900 | margin: auto; 901 | } 902 | 903 | .waiting-container p.waiting { 904 | margin-bottom: 2px; 905 | } 906 | 907 | .loading { 908 | margin: auto; 909 | display: block; 910 | width: 40%; 911 | padding-bottom: 10px; 912 | } 913 | 914 | .login-form { 915 | background: rgba(0,0,0,0.25); 916 | } 917 | .login-form.logged-in { 918 | background: transparent; 919 | } 920 | .login-form.logged-in .but-header { 921 | margin-bottom: 0; 922 | } 923 | .login-form.logged-in .btn-primary { 924 | padding-right: 50px; 925 | padding-left: 50px; 926 | font-weight: 600; 927 | } 928 | .login-form h5 { 929 | text-transform: uppercase; 930 | margin: 20px 0 15px; 931 | padding: 0; 932 | font-size: 1.2rem; 933 | } 934 | .login-form .login-btn img { 935 | height: 40px; 936 | } 937 | 938 | .line{ border-top: 1px solid #555; margin: 30px 0;} 939 | 940 | .login-callback{ 941 | height: 800px; 942 | } 943 | 944 | .case-item .wear-tier { 945 | color: #999; 946 | padding-right: 5px; 947 | display: block; 948 | } 949 | -------------------------------------------------------------------------------- /vgomock/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vgomock", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.5", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 10 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 11 | "requires": { 12 | "mime-types": "2.1.18", 13 | "negotiator": "0.6.1" 14 | } 15 | }, 16 | "any-promise": { 17 | "version": "1.3.0", 18 | "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", 19 | "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" 20 | }, 21 | "balanced-match": { 22 | "version": "1.0.0", 23 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 24 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 25 | "optional": true 26 | }, 27 | "brace-expansion": { 28 | "version": "1.1.11", 29 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 30 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 31 | "optional": true, 32 | "requires": { 33 | "balanced-match": "1.0.0", 34 | "concat-map": "0.0.1" 35 | } 36 | }, 37 | "bunyan": { 38 | "version": "1.5.1", 39 | "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", 40 | "integrity": "sha1-X259RMQ7lS9WsPQTCeOrEjkbTi0=", 41 | "requires": { 42 | "dtrace-provider": "0.6.0", 43 | "mv": "2.1.1", 44 | "safe-json-stringify": "1.1.0" 45 | } 46 | }, 47 | "co": { 48 | "version": "4.6.0", 49 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 50 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" 51 | }, 52 | "concat-map": { 53 | "version": "0.0.1", 54 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 55 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 56 | "optional": true 57 | }, 58 | "content-disposition": { 59 | "version": "0.5.2", 60 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 61 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 62 | }, 63 | "content-type": { 64 | "version": "1.0.4", 65 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 66 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 67 | }, 68 | "cookies": { 69 | "version": "0.7.1", 70 | "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.1.tgz", 71 | "integrity": "sha1-fIphX1SBxhq58WyDNzG8uPZjuZs=", 72 | "requires": { 73 | "depd": "1.1.2", 74 | "keygrip": "1.0.2" 75 | } 76 | }, 77 | "debug": { 78 | "version": "3.1.0", 79 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 80 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 81 | "requires": { 82 | "ms": "2.0.0" 83 | } 84 | }, 85 | "deep-equal": { 86 | "version": "1.0.1", 87 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 88 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" 89 | }, 90 | "delegates": { 91 | "version": "1.0.0", 92 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 93 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 94 | }, 95 | "depd": { 96 | "version": "1.1.2", 97 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 98 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 99 | }, 100 | "destroy": { 101 | "version": "1.0.4", 102 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 103 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 104 | }, 105 | "dtrace-provider": { 106 | "version": "0.6.0", 107 | "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", 108 | "integrity": "sha1-CweNVReTfYcxAUUtkUZzdVe3XlE=", 109 | "optional": true, 110 | "requires": { 111 | "nan": "2.10.0" 112 | } 113 | }, 114 | "ee-first": { 115 | "version": "1.1.1", 116 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 117 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 118 | }, 119 | "error-inject": { 120 | "version": "1.0.0", 121 | "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", 122 | "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" 123 | }, 124 | "escape-html": { 125 | "version": "1.0.3", 126 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 127 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 128 | }, 129 | "fresh": { 130 | "version": "0.5.2", 131 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 132 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 133 | }, 134 | "glob": { 135 | "version": "6.0.4", 136 | "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", 137 | "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", 138 | "optional": true, 139 | "requires": { 140 | "inflight": "1.0.6", 141 | "inherits": "2.0.3", 142 | "minimatch": "3.0.4", 143 | "once": "1.4.0", 144 | "path-is-absolute": "1.0.1" 145 | } 146 | }, 147 | "http-assert": { 148 | "version": "1.3.0", 149 | "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.3.0.tgz", 150 | "integrity": "sha1-oxpc+IyHPsu1eWkH1NbxMujAHko=", 151 | "requires": { 152 | "deep-equal": "1.0.1", 153 | "http-errors": "1.6.3" 154 | } 155 | }, 156 | "http-errors": { 157 | "version": "1.6.3", 158 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 159 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 160 | "requires": { 161 | "depd": "1.1.2", 162 | "inherits": "2.0.3", 163 | "setprototypeof": "1.1.0", 164 | "statuses": "1.5.0" 165 | } 166 | }, 167 | "inflight": { 168 | "version": "1.0.6", 169 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 170 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 171 | "optional": true, 172 | "requires": { 173 | "once": "1.4.0", 174 | "wrappy": "1.0.2" 175 | } 176 | }, 177 | "inherits": { 178 | "version": "2.0.3", 179 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 180 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 181 | }, 182 | "is-generator-function": { 183 | "version": "1.0.7", 184 | "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", 185 | "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" 186 | }, 187 | "isarray": { 188 | "version": "0.0.1", 189 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 190 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" 191 | }, 192 | "keygrip": { 193 | "version": "1.0.2", 194 | "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.2.tgz", 195 | "integrity": "sha1-rTKXxVcGneqLz+ek+kkbdcXd65E=" 196 | }, 197 | "koa": { 198 | "version": "2.5.1", 199 | "resolved": "https://registry.npmjs.org/koa/-/koa-2.5.1.tgz", 200 | "integrity": "sha512-cchwbMeG2dv3E2xTAmheDAuvR53tPgJZN/Hf1h7bTzJLSPcFZp8/t5+bNKJ6GaQZoydhZQ+1GNruhKdj3lIrug==", 201 | "requires": { 202 | "accepts": "1.3.5", 203 | "content-disposition": "0.5.2", 204 | "content-type": "1.0.4", 205 | "cookies": "0.7.1", 206 | "debug": "3.1.0", 207 | "delegates": "1.0.0", 208 | "depd": "1.1.2", 209 | "destroy": "1.0.4", 210 | "error-inject": "1.0.0", 211 | "escape-html": "1.0.3", 212 | "fresh": "0.5.2", 213 | "http-assert": "1.3.0", 214 | "http-errors": "1.6.3", 215 | "is-generator-function": "1.0.7", 216 | "koa-compose": "4.0.0", 217 | "koa-convert": "1.2.0", 218 | "koa-is-json": "1.0.0", 219 | "mime-types": "2.1.18", 220 | "on-finished": "2.3.0", 221 | "only": "0.0.2", 222 | "parseurl": "1.3.2", 223 | "statuses": "1.5.0", 224 | "type-is": "1.6.16", 225 | "vary": "1.1.2" 226 | } 227 | }, 228 | "koa-bunyan-logger": { 229 | "version": "2.0.0", 230 | "resolved": "https://registry.npmjs.org/koa-bunyan-logger/-/koa-bunyan-logger-2.0.0.tgz", 231 | "integrity": "sha1-TtkDR+mHhJ9JU/kVXeRvl/WFRM0=", 232 | "requires": { 233 | "bunyan": "1.5.1", 234 | "on-finished": "2.1.1", 235 | "uuid": "3.2.1" 236 | }, 237 | "dependencies": { 238 | "ee-first": { 239 | "version": "1.1.0", 240 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz", 241 | "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q=" 242 | }, 243 | "on-finished": { 244 | "version": "2.1.1", 245 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz", 246 | "integrity": "sha1-+CyhyeOk8yhrG5k4YQ5bhja9PLI=", 247 | "requires": { 248 | "ee-first": "1.1.0" 249 | } 250 | } 251 | } 252 | }, 253 | "koa-compose": { 254 | "version": "4.0.0", 255 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.0.0.tgz", 256 | "integrity": "sha1-KAClE9nDYe8NY4UrA45Pby1adzw=" 257 | }, 258 | "koa-convert": { 259 | "version": "1.2.0", 260 | "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", 261 | "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", 262 | "requires": { 263 | "co": "4.6.0", 264 | "koa-compose": "3.2.1" 265 | }, 266 | "dependencies": { 267 | "koa-compose": { 268 | "version": "3.2.1", 269 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", 270 | "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", 271 | "requires": { 272 | "any-promise": "1.3.0" 273 | } 274 | } 275 | } 276 | }, 277 | "koa-is-json": { 278 | "version": "1.0.0", 279 | "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", 280 | "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" 281 | }, 282 | "koa-qs": { 283 | "version": "2.0.0", 284 | "resolved": "https://registry.npmjs.org/koa-qs/-/koa-qs-2.0.0.tgz", 285 | "integrity": "sha1-GNFrQ1CKVB8JLlFDUdwJVjpIgZ8=", 286 | "requires": { 287 | "merge-descriptors": "0.0.2", 288 | "qs": "2.3.3" 289 | } 290 | }, 291 | "koa-router": { 292 | "version": "7.4.0", 293 | "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-7.4.0.tgz", 294 | "integrity": "sha512-IWhaDXeAnfDBEpWS6hkGdZ1ablgr6Q6pGdXCyK38RbzuH4LkUOpPqPw+3f8l8aTDrQmBQ7xJc0bs2yV4dzcO+g==", 295 | "requires": { 296 | "debug": "3.1.0", 297 | "http-errors": "1.6.3", 298 | "koa-compose": "3.2.1", 299 | "methods": "1.1.2", 300 | "path-to-regexp": "1.7.0", 301 | "urijs": "1.19.1" 302 | }, 303 | "dependencies": { 304 | "koa-compose": { 305 | "version": "3.2.1", 306 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", 307 | "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", 308 | "requires": { 309 | "any-promise": "1.3.0" 310 | } 311 | } 312 | } 313 | }, 314 | "koa-send": { 315 | "version": "4.1.3", 316 | "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-4.1.3.tgz", 317 | "integrity": "sha512-3UetMBdaXSiw24qM2Mx5mKmxLKw5ZTPRjACjfhK6Haca55RKm9hr/uHDrkrxhSl5/S1CKI/RivZVIopiatZuTA==", 318 | "requires": { 319 | "debug": "2.6.9", 320 | "http-errors": "1.6.3", 321 | "mz": "2.7.0", 322 | "resolve-path": "1.4.0" 323 | }, 324 | "dependencies": { 325 | "debug": { 326 | "version": "2.6.9", 327 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 328 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 329 | "requires": { 330 | "ms": "2.0.0" 331 | } 332 | } 333 | } 334 | }, 335 | "koa-static": { 336 | "version": "4.0.2", 337 | "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-4.0.2.tgz", 338 | "integrity": "sha512-tKaDVRz3lgPfdFhiYe3jNQnlSVf0AnOv7ZJqQYHkT4/kPan6b59HSmotNm2Qjl2JDlCli4xKVOMHui+fZLwNRg==", 339 | "requires": { 340 | "debug": "2.6.9", 341 | "koa-send": "4.1.3" 342 | }, 343 | "dependencies": { 344 | "debug": { 345 | "version": "2.6.9", 346 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 347 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 348 | "requires": { 349 | "ms": "2.0.0" 350 | } 351 | } 352 | } 353 | }, 354 | "media-typer": { 355 | "version": "0.3.0", 356 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 357 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 358 | }, 359 | "merge-descriptors": { 360 | "version": "0.0.2", 361 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-0.0.2.tgz", 362 | "integrity": "sha1-w2pSp4FDdRPFcnXzndnTF1FKyMc=" 363 | }, 364 | "methods": { 365 | "version": "1.1.2", 366 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 367 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 368 | }, 369 | "mime-db": { 370 | "version": "1.33.0", 371 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", 372 | "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" 373 | }, 374 | "mime-types": { 375 | "version": "2.1.18", 376 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", 377 | "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", 378 | "requires": { 379 | "mime-db": "1.33.0" 380 | } 381 | }, 382 | "minimatch": { 383 | "version": "3.0.4", 384 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 385 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 386 | "optional": true, 387 | "requires": { 388 | "brace-expansion": "1.1.11" 389 | } 390 | }, 391 | "minimist": { 392 | "version": "0.0.8", 393 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 394 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 395 | "optional": true 396 | }, 397 | "mkdirp": { 398 | "version": "0.5.1", 399 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 400 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 401 | "optional": true, 402 | "requires": { 403 | "minimist": "0.0.8" 404 | } 405 | }, 406 | "ms": { 407 | "version": "2.0.0", 408 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 409 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 410 | }, 411 | "mv": { 412 | "version": "2.1.1", 413 | "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", 414 | "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", 415 | "optional": true, 416 | "requires": { 417 | "mkdirp": "0.5.1", 418 | "ncp": "2.0.0", 419 | "rimraf": "2.4.5" 420 | } 421 | }, 422 | "mz": { 423 | "version": "2.7.0", 424 | "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", 425 | "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", 426 | "requires": { 427 | "any-promise": "1.3.0", 428 | "object-assign": "4.1.1", 429 | "thenify-all": "1.6.0" 430 | } 431 | }, 432 | "nan": { 433 | "version": "2.10.0", 434 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", 435 | "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", 436 | "optional": true 437 | }, 438 | "ncp": { 439 | "version": "2.0.0", 440 | "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", 441 | "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", 442 | "optional": true 443 | }, 444 | "negotiator": { 445 | "version": "0.6.1", 446 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 447 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 448 | }, 449 | "object-assign": { 450 | "version": "4.1.1", 451 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 452 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 453 | }, 454 | "on-finished": { 455 | "version": "2.3.0", 456 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 457 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 458 | "requires": { 459 | "ee-first": "1.1.1" 460 | } 461 | }, 462 | "once": { 463 | "version": "1.4.0", 464 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 465 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 466 | "requires": { 467 | "wrappy": "1.0.2" 468 | } 469 | }, 470 | "only": { 471 | "version": "0.0.2", 472 | "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", 473 | "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" 474 | }, 475 | "parseurl": { 476 | "version": "1.3.2", 477 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 478 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 479 | }, 480 | "path-is-absolute": { 481 | "version": "1.0.1", 482 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 483 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 484 | }, 485 | "path-to-regexp": { 486 | "version": "1.7.0", 487 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", 488 | "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", 489 | "requires": { 490 | "isarray": "0.0.1" 491 | } 492 | }, 493 | "qs": { 494 | "version": "2.3.3", 495 | "resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz", 496 | "integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ=" 497 | }, 498 | "resolve-path": { 499 | "version": "1.4.0", 500 | "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", 501 | "integrity": "sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc=", 502 | "requires": { 503 | "http-errors": "1.6.3", 504 | "path-is-absolute": "1.0.1" 505 | } 506 | }, 507 | "rimraf": { 508 | "version": "2.4.5", 509 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", 510 | "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", 511 | "optional": true, 512 | "requires": { 513 | "glob": "6.0.4" 514 | } 515 | }, 516 | "safe-json-stringify": { 517 | "version": "1.1.0", 518 | "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.1.0.tgz", 519 | "integrity": "sha512-EzBtUaFH9bHYPc69wqjp0efJI/DPNHdFbGE3uIMn4sVbO0zx8vZ8cG4WKxQfOpUOKsQyGBiT2mTqnCw+6nLswA==", 520 | "optional": true 521 | }, 522 | "setprototypeof": { 523 | "version": "1.1.0", 524 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 525 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 526 | }, 527 | "statuses": { 528 | "version": "1.5.0", 529 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 530 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 531 | }, 532 | "thenify": { 533 | "version": "3.3.0", 534 | "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", 535 | "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", 536 | "requires": { 537 | "any-promise": "1.3.0" 538 | } 539 | }, 540 | "thenify-all": { 541 | "version": "1.6.0", 542 | "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", 543 | "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", 544 | "requires": { 545 | "thenify": "3.3.0" 546 | } 547 | }, 548 | "type-is": { 549 | "version": "1.6.16", 550 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 551 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 552 | "requires": { 553 | "media-typer": "0.3.0", 554 | "mime-types": "2.1.18" 555 | } 556 | }, 557 | "urijs": { 558 | "version": "1.19.1", 559 | "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", 560 | "integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==" 561 | }, 562 | "uuid": { 563 | "version": "3.2.1", 564 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", 565 | "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" 566 | }, 567 | "vary": { 568 | "version": "1.1.2", 569 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 570 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 571 | }, 572 | "wrappy": { 573 | "version": "1.0.2", 574 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 575 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 576 | } 577 | } 578 | } 579 | --------------------------------------------------------------------------------