From d684a9c5fc1dd6700d1a1e6ce127c5b861b7a685 Mon Sep 17 00:00:00 2001 From: Jeremy Letto Date: Mon, 25 Oct 2021 13:28:43 -0500 Subject: [PATCH] new login design (#4442) * new login design * remove effects file (wrong direction) * add Known Hosts dropdown component Co-authored-by: Jeremy Letto --- webclient/src/components/Guard/AuthGuard.tsx | 2 +- .../src/components/InputField/InputField.css | 20 ++ .../src/components/InputField/InputField.tsx | 52 +++-- .../src/components/KnownHosts/KnownHosts.css | 3 + .../src/components/KnownHosts/KnownHosts.tsx | 79 ++++++++ webclient/src/components/index.ts | 1 + .../src/containers/App/AppShellRoutes.tsx | 4 +- webclient/src/containers/Login/Login.css | 72 +++++++ webclient/src/containers/Login/Login.tsx | 97 ++++++++++ webclient/src/containers/Room/SayMessage.tsx | 2 +- webclient/src/containers/Server/Server.css | 29 +-- webclient/src/containers/Server/Server.tsx | 146 ++++---------- webclient/src/containers/index.ts | 3 +- webclient/src/forms/LoginForm/LoginForm.css | 20 ++ webclient/src/forms/LoginForm/LoginForm.tsx | 73 +++++++ webclient/src/forms/index.ts | 1 + webclient/src/images/logo.png | Bin 0 -> 25695 bytes webclient/src/index.css | 2 + webclient/src/material-theme.ts | 180 +++++++++++++----- webclient/src/services/DexieDTOs/HostDTO.ts | 28 +++ webclient/src/services/DexieDTOs/index.ts | 1 + webclient/src/services/DexieService.ts | 12 +- webclient/src/types/forms.ts | 1 + webclient/src/types/routes.tsx | 17 +- webclient/src/types/server.tsx | 42 ++++ 25 files changed, 675 insertions(+), 212 deletions(-) create mode 100644 webclient/src/components/InputField/InputField.css create mode 100644 webclient/src/components/KnownHosts/KnownHosts.css create mode 100644 webclient/src/components/KnownHosts/KnownHosts.tsx create mode 100644 webclient/src/containers/Login/Login.css create mode 100644 webclient/src/containers/Login/Login.tsx create mode 100644 webclient/src/forms/LoginForm/LoginForm.css create mode 100644 webclient/src/forms/LoginForm/LoginForm.tsx create mode 100644 webclient/src/images/logo.png create mode 100644 webclient/src/services/DexieDTOs/HostDTO.ts diff --git a/webclient/src/components/Guard/AuthGuard.tsx b/webclient/src/components/Guard/AuthGuard.tsx index 3dc82d12..24345526 100644 --- a/webclient/src/components/Guard/AuthGuard.tsx +++ b/webclient/src/components/Guard/AuthGuard.tsx @@ -9,7 +9,7 @@ import { AuthenticationService } from "api"; const AuthGuard = ({ state }: AuthGuardProps) => { return !AuthenticationService.isConnected(state) - ? + ? :
; }; diff --git a/webclient/src/components/InputField/InputField.css b/webclient/src/components/InputField/InputField.css new file mode 100644 index 00000000..7ddfd0de --- /dev/null +++ b/webclient/src/components/InputField/InputField.css @@ -0,0 +1,20 @@ +.inputField { + position: relative; +} + +.inputField-validation { + position: absolute; + top: 0; + right: 0; + transform: translateY(-50%); + font-weight: bold; +} + +.inputField-error { + display: flex; + align-items: center; +} + +.inputField-error svg { + margin-left: 4px; +} diff --git a/webclient/src/components/InputField/InputField.tsx b/webclient/src/components/InputField/InputField.tsx index 3cf16c63..bbfefed9 100644 --- a/webclient/src/components/InputField/InputField.tsx +++ b/webclient/src/components/InputField/InputField.tsx @@ -1,17 +1,47 @@ import React from "react"; +import { styled } from '@material-ui/core/styles'; import TextField from "@material-ui/core/TextField"; +import ErrorOutlinedIcon from '@material-ui/icons/ErrorOutlined'; -const InputField = ({ input, label, name, autoComplete, type }) => ( - +import './InputField.css'; + +const InputField = ({ input, label, name, autoComplete, type, meta: { touched, error, warning } }) => ( +
+ { touched && ( +
+ { + ( error && + + {error} + + + ) || + + ( warning && {warning} ) + } +
+ ) } + + +
); +const ThemedFieldError = styled('div')(({ theme }) => ({ + color: theme.palette.error.main +})); + +const ThemedFieldWarning = styled('div')(({ theme }) => ({ + color: theme.palette.warning.main +})); + export default InputField; \ No newline at end of file diff --git a/webclient/src/components/KnownHosts/KnownHosts.css b/webclient/src/components/KnownHosts/KnownHosts.css new file mode 100644 index 00000000..4b499b52 --- /dev/null +++ b/webclient/src/components/KnownHosts/KnownHosts.css @@ -0,0 +1,3 @@ +.KnownHosts { + width: 100%; +} diff --git a/webclient/src/components/KnownHosts/KnownHosts.tsx b/webclient/src/components/KnownHosts/KnownHosts.tsx new file mode 100644 index 00000000..3783564f --- /dev/null +++ b/webclient/src/components/KnownHosts/KnownHosts.tsx @@ -0,0 +1,79 @@ +// eslint-disable-next-line +import React, { useEffect, useState } from 'react'; +import { Select, MenuItem } from '@material-ui/core'; +import Button from '@material-ui/core/Button'; +import FormControl from '@material-ui/core/FormControl'; +import IconButton from '@material-ui/core/IconButton'; +import InputLabel from '@material-ui/core/InputLabel'; +import EditRoundedIcon from '@material-ui/icons/Edit'; + +import { HostDTO } from 'services'; +import { DefaultHosts, getHostPort } from 'types'; + +import './KnownHosts.css'; + +const KnownHosts = ({ onChange }) => { + const [state, setState] = useState({ + hosts: [], + selectedHost: 0, + }); + + useEffect(() => { + HostDTO.getAll().then(async hosts => { + if (hosts?.length) { + setState(s => ({ ...s, hosts })); + } else { + setState(s => ({ ...s, hosts: DefaultHosts })); + await HostDTO.bulkAdd(DefaultHosts); + } + }); + }, []); + + useEffect(() => { + if (state.hosts.length) { + onChange(getHostPort(state.hosts[state.selectedHost])); + } + }, [state, onChange]); + + const selectHost = (selectedHost) => { + setState(s => ({ ...s, selectedHost })); + }; + + const addKnownHost = () => { + console.log('KnownHosts->addKnownHost'); + }; + + const editKnownHost = (hostIndex) => { + console.log('KnownHosts->editKnownHost: ', state.hosts[hostIndex]); + }; + + return ( + + Host + + + ) +}; + +export default KnownHosts; diff --git a/webclient/src/components/index.ts b/webclient/src/components/index.ts index 4fcf62bb..5f392f56 100644 --- a/webclient/src/components/index.ts +++ b/webclient/src/components/index.ts @@ -4,6 +4,7 @@ export { default as CardDetails } from './CardDetails/CardDetails'; export { default as Header } from './Header/Header'; export { default as InputField } from './InputField/InputField'; export { default as InputAction } from './InputAction/InputAction'; +export { default as KnownHosts } from './KnownHosts/KnownHosts'; export { default as Message } from './Message/Message'; export { default as VirtualList } from './VirtualList/VirtualList'; export { default as UserDisplay} from './UserDisplay/UserDisplay'; diff --git a/webclient/src/containers/App/AppShellRoutes.tsx b/webclient/src/containers/App/AppShellRoutes.tsx index 61a6b68e..414dfc6a 100644 --- a/webclient/src/containers/App/AppShellRoutes.tsx +++ b/webclient/src/containers/App/AppShellRoutes.tsx @@ -9,6 +9,7 @@ import { Player, Room, Server, + Login, Logs } from "containers"; @@ -22,7 +23,8 @@ const Routes = () => ( } /> { } />} } /> - + } /> + ); diff --git a/webclient/src/containers/Login/Login.css b/webclient/src/containers/Login/Login.css new file mode 100644 index 00000000..2258ae1d --- /dev/null +++ b/webclient/src/containers/Login/Login.css @@ -0,0 +1,72 @@ +.login { + height: 100%; + padding: 50px; +} + +.login__wrapper { + display: flex; + flex-direction: column; + align-items: center; +} + +.login-content { + width: 100%; + max-width: 1000px; + display: flex; + border-radius: 8px; + overflow: hidden; +} + +.login-content__header { + font-family: Teko, sans-serif; + font-size: 34px; + font-weight: bold; + display: flex; + align-items: center; +} + +.login-content__header img { + height: 60px; + margin-right: 15px; + margin-bottom: 20px; +} + +.login-content__form, +.login-content__description { + width: 50%; +} + +.login-content__form { + padding: 50px 50px 33px; +} + +.login-content__form h1 { + margin-bottom: 20px; +} + +.login-form { + margin-top: 30px; +} + +.login-content__description { + display: flex; + justify-content: center; + align-items: center; + font-size: 24px; + text-transform: uppercase; +} + +.login-footer { + margin-top: 30px; +} + +.login-footer_register { + margin-bottom: 10px; + font-weight: bold; +} + +.login-content__connectionStatus { + text-align: center; + margin: 20px 0; + padding: 20px; +} diff --git a/webclient/src/containers/Login/Login.tsx b/webclient/src/containers/Login/Login.tsx new file mode 100644 index 00000000..5e43ba3d --- /dev/null +++ b/webclient/src/containers/Login/Login.tsx @@ -0,0 +1,97 @@ +// eslint-disable-next-line +import React from "react"; +import { connect } from "react-redux"; +import { Redirect } from "react-router-dom"; +import { styled } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import Paper from '@material-ui/core/Paper'; +import Typography from '@material-ui/core/Typography'; + +import { AuthenticationService } from "api"; +import { LoginForm } from "forms"; +import { RouteEnum } from "types"; +import { /* ServerDispatch, */ ServerSelectors } from "store"; + +import "./Login.css"; +import logo from "images/logo.png"; + +const Login = ({ state, description }: LoginProps) => { + const isConnected = AuthenticationService.isConnected(state); + + const showDescription = () => { + return !isConnected && description?.length; + } + + const createAccount = () => { + console.log('Login.createAccount->openForgotPasswordDialog'); + }; + + return ( +
+ { isConnected && } + +
+ +
+ + logo + COCKATRICE + + Login + A cross-platform virtual tabletop for multiplayer card games. +
+ +
+ + { + showDescription() && ( + + {description} + + ) + } + +
+
+ Not registered yet? + +
+ + Cockatrice is an open source project @{ new Date().getUTCFullYear() } + +
+
+ + + description + +
+
+
+ ); +} + +const ThemedLoginContent = styled('div')(({ theme }) => ({ + backgroundColor: theme.palette.background.paper +})); + +const ThemedLoginDescription = styled('div')(({ theme }) => ({ + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, +})); + +const ThemedLoginHeader = styled('div')(({ theme }) => ({ + color: theme.palette.success.light +})); + +interface LoginProps { + state: number; + description: string; +} + +const mapStateToProps = state => ({ + state: ServerSelectors.getState(state), + description: ServerSelectors.getDescription(state), +}); + +export default connect(mapStateToProps)(Login); diff --git a/webclient/src/containers/Room/SayMessage.tsx b/webclient/src/containers/Room/SayMessage.tsx index 2c33825c..382f3f33 100644 --- a/webclient/src/containers/Room/SayMessage.tsx +++ b/webclient/src/containers/Room/SayMessage.tsx @@ -7,7 +7,7 @@ import { InputAction } from 'components'; const SayMessage = ({ handleSubmit }) => (
- + ); diff --git a/webclient/src/containers/Server/Server.css b/webclient/src/containers/Server/Server.css index ea5e2dec..8d4e2e8d 100644 --- a/webclient/src/containers/Server/Server.css +++ b/webclient/src/containers/Server/Server.css @@ -10,33 +10,6 @@ align-items: center; } -.server .form-wrapper { - display: flex; - flex-direction: column; -} - -.server-connect { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; -} - -.server-connect__form, -.server-connect__description { - width: 100%; - max-width: 300px; -} - -.server-connect__form { - margin: 50px 0; -} - -.server-connect__description { - text-align: center; - margin: 20px 0; - padding: 20px; -} .serverRoomWrapper { height: 100%; @@ -58,4 +31,4 @@ padding: 10px; background: white; z-index: 1; -} \ No newline at end of file +} diff --git a/webclient/src/containers/Server/Server.tsx b/webclient/src/containers/Server/Server.tsx index 5f3edf4a..4ced89ed 100644 --- a/webclient/src/containers/Server/Server.tsx +++ b/webclient/src/containers/Server/Server.tsx @@ -3,135 +3,59 @@ import React, { Component } from "react"; import { connect } from "react-redux"; import { withRouter } from "react-router-dom"; -import Button from "@material-ui/core/Button"; import ListItem from "@material-ui/core/ListItem"; import Paper from "@material-ui/core/Paper"; import { RoomsSelectors, ServerSelectors } from "store"; -import { AuthenticationService } from "api"; - import { ThreePaneLayout, UserDisplay, VirtualList } from "components"; -import { ConnectForm, RegisterForm } from "forms"; -import { Room, StatusEnum, User } from "types"; +import { Room, User } from "types"; import Rooms from './Rooms'; import "./Server.css"; class Server extends Component { - constructor(props) { - super(props); - - this.showDescription = this.showDescription.bind(this); - this.showRegisterForm = this.showRegisterForm.bind(this); - this.hideRegisterForm = this.hideRegisterForm.bind(this); - - this.state = { - register: false - }; - } - - showDescription(state, description) { - const isDisconnected = state === StatusEnum.DISCONNECTED; - const hasDescription = description && !!description.length; - - return isDisconnected && hasDescription; - } - - showRegisterForm() { - this.setState({register: true}); - } - - hideRegisterForm() { - this.setState({register: false}); - } - render() { - const { message, rooms, joinedRooms, history, state, description, users } = this.props; - const { register } = this.state; - const isConnected = AuthenticationService.isConnected(state); + const { message, rooms, joinedRooms, history, users } = this.props; return ( -
- { - isConnected - ? ( ) - : ( -
- - { - register - ? ( ) - : ( ) - } - -
- ) - } - { - !isConnected && this.showDescription(state, description) && ( - - {description} - - ) - } +
+ + + + )} + + bottom={( + +
+ + )} + + side={( + +
+ Users connected to server: {users.length} +
+ users[index].name } + items={ users.map(user => ( + + + + ) ) } + /> +
+ )} + />
); } } -const ServerRooms = ({ rooms, joinedRooms, history, message, users}) => ( -
- - - - )} - - bottom={( - -
- - )} - - side={( - -
- Users connected to server: {users.length} -
- users[index].name } - items={ users.map(user => ( - - - - ) ) } - /> -
- )} - /> -
-); - -const Connect = ({register}) => ( -
- - -
-); - -const Register = ({ connect }) => ( -
- - -
-); - interface ServerProps { message: string; - state: number; - description: string; rooms: Room[]; joinedRooms: Room[]; users: User[]; @@ -139,16 +63,14 @@ interface ServerProps { } interface ServerState { - register: boolean; + } const mapStateToProps = state => ({ message: ServerSelectors.getMessage(state), - state: ServerSelectors.getState(state), - description: ServerSelectors.getDescription(state), rooms: RoomsSelectors.getRooms(state), joinedRooms: RoomsSelectors.getJoinedRooms(state), users: ServerSelectors.getUsers(state) }); -export default withRouter(connect(mapStateToProps)(Server)); \ No newline at end of file +export default withRouter(connect(mapStateToProps)(Server)); diff --git a/webclient/src/containers/index.ts b/webclient/src/containers/index.ts index f39d3725..e9efbd6f 100644 --- a/webclient/src/containers/index.ts +++ b/webclient/src/containers/index.ts @@ -5,4 +5,5 @@ export { default as Decks } from './Decks/Decks'; export { default as Room } from "./Room/Room"; export { default as Player } from "./Player/Player"; export { default as Server } from "./Server/Server"; -export { default as Logs } from "./Logs/Logs"; \ No newline at end of file +export { default as Logs } from "./Logs/Logs"; +export { default as Login } from "./Login/Login"; diff --git a/webclient/src/forms/LoginForm/LoginForm.css b/webclient/src/forms/LoginForm/LoginForm.css new file mode 100644 index 00000000..4b176476 --- /dev/null +++ b/webclient/src/forms/LoginForm/LoginForm.css @@ -0,0 +1,20 @@ +.loginForm { + width: 100%; +} + +.loginForm-item { + margin-bottom: 20px; +} + +.loginForm-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: -20px; + margin-bottom: 20px; + font-weight: bold; +} + +.loginForm-submit { + width: 100%; +} diff --git a/webclient/src/forms/LoginForm/LoginForm.tsx b/webclient/src/forms/LoginForm/LoginForm.tsx new file mode 100644 index 00000000..0ce75366 --- /dev/null +++ b/webclient/src/forms/LoginForm/LoginForm.tsx @@ -0,0 +1,73 @@ +// eslint-disable-next-line +import React from "react"; +import { connect } from "react-redux"; +import { Form, Field, reduxForm, change } from "redux-form" + +import Button from "@material-ui/core/Button"; + +import { InputField, KnownHosts } from "components"; +// import { ServerDispatch } from "store"; +import { FormKey } from 'types'; + +import "./LoginForm.css"; + +const LoginForm = (props) => { + const { dispatch, handleSubmit } = props; + + const forgotPassword = () => { + console.log('LoginForm.forgotPassword->openForgotPasswordDialog'); + }; + + const onHostChange = ({ host, port }) => { + dispatch(change(FormKey.LOGIN, 'host', host)); + dispatch(change(FormKey.LOGIN, 'port', port)); + } + + return ( +
+
+
+ +
+
+ +
+
+ Auto Connect + +
+
+ +
+
+ +
+ ); +} + +const propsMap = { + form: FormKey.LOGIN, + validate: values => { + const errors: any = {}; + + if (!values.user) errors.user = 'Required'; + if (!values.pass) errors.pass = 'Required'; + if (!values.host) errors.host = 'Required'; + if (!values.port) errors.port = 'Required'; + + return errors; + } +}; + +const mapStateToProps = () => ({ + initialValues: { + // host: "mtg.tetrarch.co/servatrice", + // port: "443" + // host: "server.cockatrice.us", + // port: "4748" + } +}); + +export default connect(mapStateToProps)(reduxForm(propsMap)(LoginForm)); diff --git a/webclient/src/forms/index.ts b/webclient/src/forms/index.ts index 72fa688e..141ff1be 100644 --- a/webclient/src/forms/index.ts +++ b/webclient/src/forms/index.ts @@ -1,4 +1,5 @@ export { default as CardImportForm } from './CardImportForm/CardImportForm'; export { default as ConnectForm } from './ConnectForm/ConnectForm'; +export { default as LoginForm } from './LoginForm/LoginForm'; export { default as RegisterForm } from './RegisterForm/RegisterForm'; export { default as SearchForm } from './SearchForm/SearchForm'; diff --git a/webclient/src/images/logo.png b/webclient/src/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7ce83bd208bbe08818a0a3859e5d0d87a0c71f92 GIT binary patch literal 25695 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 aM*a_U9{k5Cga .MuiButtonBase-root': { + padding: '8px 16px', + marginBottom: '4px', + borderRadius: 0, + justifyContent: 'space-between', + }, + + '& .MuiButtonBase-root.Mui-selected, & .MuiButtonBase-root.Mui-selected:hover, & .MuiButtonBase-root:hover': { + background: palette.primary.light + }, + + [[ + '& .MuiButtonBase-root.Mui-selected', + '& .MuiButtonBase-root.Mui-selected:hover', + '& .MuiButtonBase-root:hover' + ].join(', ')]: { + background: palette.primary.light + }, + }, + }, + + MuiListItem: { + root: { + }, + }, + + MuiInputBase: { + formControl: { + '& .MuiSelect-root svg': { + display: 'none', + }, + }, + }, + + MuiOutlinedInput: { + root: { + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderWidth: '1px', + }, + + '.rounded &': { + // 'border-radius': '50px', + }, + + '.tall &': { + height: '40px', + }, + }, }, - // secondary: { - // main: '', - // light: '', - // dark: '', - // contrastText: '', - // }, - // error: { - // main: '', - // light: '', - // dark: '', - // contrastText: '', - // }, - // warning: { - // main: '', - // light: '', - // dark: '', - // contrastText: '', - // }, - // info: { - // main: '', - // light: '', - // dark: '', - // contrastText: '', - // }, - // success: { - // main: '', - // light: '', - // dark: '', - // contrastText: '', - // }, }, typography: { fontSize: 12, - // h1: {}, + h1: { + fontSize: 28, + fontWeight: 'bold', + }, // h2: {}, // h3: {}, // h4: {}, // h5: {}, // h6: {}, - // subtitle1: {}, - // subtitle2: {}, + subtitle1: { + fontSize: 14, + fontWeight: 'bold', + lineHeight: 1.4, + color: '#9E9E9E', + }, + subtitle2: { + lineHeight: 1.4, + color: '#9E9E9E', + }, // body1: {}, // body2: {}, // button: {}, diff --git a/webclient/src/services/DexieDTOs/HostDTO.ts b/webclient/src/services/DexieDTOs/HostDTO.ts new file mode 100644 index 00000000..e58c8231 --- /dev/null +++ b/webclient/src/services/DexieDTOs/HostDTO.ts @@ -0,0 +1,28 @@ +import { IndexableType } from 'dexie'; +import { Host } from 'types'; + +import { dexieService } from '../DexieService'; + +export class HostDTO extends Host { + save() { + return dexieService.hosts.put(this); + } + + static add(host: HostDTO): Promise { + return dexieService.hosts.add(host); + } + + static get(id): Promise { + return dexieService.hosts.where('id').equals(id).first(); + } + + static getAll(): Promise { + return dexieService.hosts.toArray(); + } + + static bulkAdd(hosts: Host[]): Promise { + return dexieService.hosts.bulkAdd(hosts); + } +}; + +dexieService.hosts.mapToClass(HostDTO); diff --git a/webclient/src/services/DexieDTOs/index.ts b/webclient/src/services/DexieDTOs/index.ts index 2ec93016..f95954c5 100644 --- a/webclient/src/services/DexieDTOs/index.ts +++ b/webclient/src/services/DexieDTOs/index.ts @@ -1,3 +1,4 @@ export * from './CardDTO'; export * from './SetDTO'; export * from './TokenDTO'; +export * from './HostDTO'; diff --git a/webclient/src/services/DexieService.ts b/webclient/src/services/DexieService.ts index 2429481b..b9141ddb 100644 --- a/webclient/src/services/DexieService.ts +++ b/webclient/src/services/DexieService.ts @@ -4,12 +4,14 @@ enum Stores { CARDS = 'cards', SETS = 'sets', TOKENS = 'tokens', + HOSTS = 'hosts', } const StoreKeyIndexes = { - [Stores.CARDS]: "name", - [Stores.SETS]: "code", - [Stores.TOKENS]: "name.value", + [Stores.CARDS]: 'name', + [Stores.SETS]: 'code', + [Stores.TOKENS]: 'name.value', + [Stores.HOSTS]: '++id,name', }; class DexieService { @@ -30,6 +32,10 @@ class DexieService { get tokens() { return this.db.table(Stores.TOKENS); } + + get hosts() { + return this.db.table(Stores.HOSTS); + } } export const dexieService = new DexieService(); diff --git a/webclient/src/types/forms.ts b/webclient/src/types/forms.ts index 753885ea..32cf7e40 100644 --- a/webclient/src/types/forms.ts +++ b/webclient/src/types/forms.ts @@ -3,6 +3,7 @@ export enum FormKey { ADD_TO_IGNORE = "ADD_TO_IGNORE", CARD_IMPORT = "CARD_IMPORT", CONNECT = "CONNECT", + LOGIN = "LOGIN", REGISTER = "REGISTER", SEARCH_LOGS = "SEARCH_LOGS", } diff --git a/webclient/src/types/routes.tsx b/webclient/src/types/routes.tsx index e5e1bf63..0dd3bba6 100644 --- a/webclient/src/types/routes.tsx +++ b/webclient/src/types/routes.tsx @@ -1,10 +1,11 @@ export enum RouteEnum { - PLAYER = "/player/:name", - SERVER = "/server", - ROOM = "/room/:roomId", - LOGS = "/logs", - GAME = "/game", - DECKS = "/decks", - DECK = "/deck", - ACCOUNT = "/account", + PLAYER = '/player/:name', + SERVER = '/server', + ROOM = '/room/:roomId', + LOGIN = '/', + LOGS = '/logs', + GAME = '/game', + DECKS = '/decks', + DECK = '/deck', + ACCOUNT = '/account', } diff --git a/webclient/src/types/server.tsx b/webclient/src/types/server.tsx index 23a70d25..d3a15881 100644 --- a/webclient/src/types/server.tsx +++ b/webclient/src/types/server.tsx @@ -31,6 +31,48 @@ export enum StatusEnumLabel { "Disconnecting" = 99 } +export class Host { + id?: number; + name: string; + host: string; + port: string; + localHost?: string; + localPort?: string; + editable: boolean; +} + +export const DefaultHosts: Host[] = [ + { + name: 'Rooster', + host: 'server.cockatrice.us/servatrice', + port: '4748', + localHost: 'server.cockatrice.us', + editable: false, + }, + { + name: 'Tetrarch', + host: 'mtg.tetrarch.co/servatrice', + port: '4748', + editable: false, + }, +]; + +export const getHostPort = (host: Host): { host: string, port: string } => { + const isLocal = window.location.hostname === 'localhost'; + + if (!host) { + return { + host: '', + port: '' + }; + } + + return { + host: !isLocal ? host.host : host.localHost || host.host, + port: !isLocal ? host.port : host.localPort || host.port, + } +}; + export enum KnownHost { ROOSTER = 'Rooster', TETRARCH = 'Tetrarch',