diff options
author | Dessalines <tyhou13@gmx.com> | 2019-03-20 18:22:31 -0700 |
---|---|---|
committer | Dessalines <tyhou13@gmx.com> | 2019-03-20 18:22:31 -0700 |
commit | 816aa0b15f3766e340d8722f03e8b3a7633ab6fb (patch) | |
tree | 23dd0fc329e8f08c71dc6f10dd398b35d92c047c /ui/src | |
parent | 064d7f84b25236195eeb33a8671935bc9df37e57 (diff) |
Adding initial UI and Websocket server.
Diffstat (limited to 'ui/src')
-rw-r--r-- | ui/src/components/home.tsx | 14 | ||||
-rw-r--r-- | ui/src/components/login.tsx | 145 | ||||
-rw-r--r-- | ui/src/components/navbar.tsx | 38 | ||||
-rw-r--r-- | ui/src/components/search.tsx | 205 | ||||
-rw-r--r-- | ui/src/env.ts | 3 | ||||
-rw-r--r-- | ui/src/index.html | 19 | ||||
-rw-r--r-- | ui/src/index.tsx | 42 | ||||
-rw-r--r-- | ui/src/interfaces.ts | 14 | ||||
-rw-r--r-- | ui/src/main.css | 0 | ||||
-rw-r--r-- | ui/src/services.ts | 57 | ||||
-rw-r--r-- | ui/src/utils.ts | 2 |
11 files changed, 539 insertions, 0 deletions
diff --git a/ui/src/components/home.tsx b/ui/src/components/home.tsx new file mode 100644 index 00000000..07cb94f5 --- /dev/null +++ b/ui/src/components/home.tsx @@ -0,0 +1,14 @@ +import { Component } from 'inferno'; +import { repoUrl } from '../utils'; + +export class Home extends Component<any, any> { + + render() { + return ( + <div class="container"> + hola this is me. + </div> + ) + } + +} diff --git a/ui/src/components/login.tsx b/ui/src/components/login.tsx new file mode 100644 index 00000000..fd6f5045 --- /dev/null +++ b/ui/src/components/login.tsx @@ -0,0 +1,145 @@ +import { Component, linkEvent } from 'inferno'; + +import { LoginForm, RegisterForm } from '../interfaces'; +import { WebSocketService } from '../services'; + +interface State { + loginForm: LoginForm; + registerForm: RegisterForm; +} + +let emptyState: State = { + loginForm: { + username: null, + password: null + }, + registerForm: { + username: null, + password: null, + password_verify: null + } +} + +export class Login extends Component<any, State> { + + constructor(props, context) { + super(props, context); + + this.state = emptyState; + + } + render() { + return ( + <div class="container"> + <div class="row"> + <div class="col-12 col-lg-6 mb-4"> + {this.loginForm()} + </div> + <div class="col-12 col-lg-6"> + {this.registerForm()} + </div> + </div> + </div> + ) + } + + loginForm() { + return ( + <div> + <form onSubmit={linkEvent(this, this.handleLoginSubmit)}> + <h3>Login</h3> + <div class="form-group row"> + <label class="col-sm-2 col-form-label">Email or Username</label> + <div class="col-sm-10"> + <input type="text" class="form-control" value={this.state.loginForm.username} onInput={linkEvent(this, this.handleLoginUsernameChange)} required minLength={3} /> + </div> + </div> + <div class="form-group row"> + <label class="col-sm-2 col-form-label">Password</label> + <div class="col-sm-10"> + <input type="password" value={this.state.loginForm.password} onInput={linkEvent(this, this.handleLoginPasswordChange)} class="form-control" required /> + </div> + </div> + <div class="form-group row"> + <div class="col-sm-10"> + <button type="submit" class="btn btn-secondary">Login</button> + </div> + </div> + </form> + Forgot your password or deleted your account? Reset your password. TODO + </div> + ); + } + registerForm() { + return ( + <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}> + <h3>Sign Up</h3> + <div class="form-group row"> + <label class="col-sm-2 col-form-label">Username</label> + <div class="col-sm-10"> + <input type="text" class="form-control" value={this.state.registerForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} /> + </div> + </div> + <div class="form-group row"> + <label class="col-sm-2 col-form-label">Email</label> + <div class="col-sm-10"> + <input type="email" class="form-control" value={this.state.registerForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} /> + </div> + </div> + <div class="form-group row"> + <label class="col-sm-2 col-form-label">Password</label> + <div class="col-sm-10"> + <input type="password" value={this.state.registerForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required /> + </div> + </div> + <div class="form-group row"> + <label class="col-sm-2 col-form-label">Verify Password</label> + <div class="col-sm-10"> + <input type="password" value={this.state.registerForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required /> + </div> + </div> + <div class="form-group row"> + <div class="col-sm-10"> + <button type="submit" class="btn btn-secondary">Sign Up</button> + </div> + </div> + </form> + ); + } + + handleLoginSubmit(i: Login, event) { + console.log(i.state); + event.preventDefault(); + WebSocketService.Instance.login(i.state.loginForm); + } + + handleLoginUsernameChange(i: Login, event) { + i.state.loginForm.username = event.target.value; + } + + handleLoginPasswordChange(i: Login, event) { + i.state.loginForm.password = event.target.value; + } + + handleRegisterSubmit(i: Login, event) { + console.log(i.state); + event.preventDefault(); + WebSocketService.Instance.register(i.state.registerForm); + } + + handleRegisterUsernameChange(i: Login, event) { + i.state.registerForm.username = event.target.value; + } + + handleRegisterEmailChange(i: Login, event) { + i.state.registerForm.email = event.target.value; + } + + handleRegisterPasswordChange(i: Login, event) { + i.state.registerForm.password = event.target.value; + } + + handleRegisterPasswordVerifyChange(i: Login, event) { + i.state.registerForm.password_verify = event.target.value; + } +} diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx new file mode 100644 index 00000000..86d5d1d2 --- /dev/null +++ b/ui/src/components/navbar.tsx @@ -0,0 +1,38 @@ +import { Component, linkEvent } from 'inferno'; +import { Link } from 'inferno-router'; +import { repoUrl } from '../utils'; + +export class Navbar extends Component<any, any> { + + constructor(props, context) { + super(props, context); + } + + render() { + return ( + <div class="sticky-top">{this.navbar()}</div> + ) + } + + // TODO class active corresponding to current page + navbar() { + return ( + <nav class="navbar navbar-light bg-light p-0 px-3 shadow"> + <a class="navbar-brand mx-1" href="#"> + rrf + </a> + <ul class="navbar-nav mr-auto"> + <li class="nav-item"> + <a class="nav-item nav-link" href={repoUrl}>github</a> + </li> + </ul> + <ul class="navbar-nav ml-auto mr-2"> + <li class="nav-item"> + <Link class="nav-item nav-link" to="/login">Login</Link> + </li> + </ul> + </nav> + ); + } + +} diff --git a/ui/src/components/search.tsx b/ui/src/components/search.tsx new file mode 100644 index 00000000..080761f9 --- /dev/null +++ b/ui/src/components/search.tsx @@ -0,0 +1,205 @@ +import { Component, linkEvent } from 'inferno'; +import * as moment from 'moment'; + +import { endpoint } from '../env'; +import { SearchParams, Results, Torrent } from '../interfaces'; +import { humanFileSize, magnetLink, getFileName } from '../utils'; + +interface State { + results: Results; + searchParams: SearchParams; + searching: Boolean; +} + +export class Search extends Component<any, State> { + + state: State = { + results: { + torrents: [] + }, + searchParams: { + q: "", + page: 1, + type_: 'torrent' + }, + searching: false + }; + + constructor(props, context) { + super(props, context); + } + + componentDidMount() { + this.state.searchParams = { + page: Number(this.props.match.params.page), + q: this.props.match.params.q, + type_: this.props.match.params.type_ + } + this.search(); + } + + // Re-do search if the props have changed + componentDidUpdate(lastProps, lastState, snapshot) { + if (lastProps.match && lastProps.match.params !== this.props.match.params) { + this.state.searchParams = { + page: Number(this.props.match.params.page), + q: this.props.match.params.q, + type_: this.props.match.params.type_ + } + this.search(); + } + + } + + search() { + if (!!this.state.searchParams.q) { + this.setState({ searching: true, results: { torrents: [] } }); + this.fetchData(this.state.searchParams) + .then(torrents => { + if (!!torrents) { + this.setState({ + results: { + torrents: torrents + } + }); + } + }).catch(error => { + console.error('request failed', error); + }).then(() => this.setState({ searching: false })); + } else { + this.setState({ results: { torrents: [] } }); + } + } + + fetchData(searchParams: SearchParams): Promise<Array<Torrent>> { + let q = encodeURI(searchParams.q); + return fetch(`${endpoint}/service/search?q=${q}&page=${searchParams.page}&type_=${searchParams.type_}`) + .then(data => data.json()); + } + + render() { + return ( + <div> + { + this.state.searching ? + this.spinner() : this.state.results.torrents[0] ? + this.torrentsTable() + : this.noResults() + } + </div> + ); + } + + spinner() { + return ( + <div class="text-center m-5 p-5"> + <svg class="icon icon-spinner spinner"><use xlinkHref="#icon-spinner"></use></svg> + </div> + ); + } + + noResults() { + return ( + <div class="text-center m-5 p-5"> + <h1>No Results</h1> + </div> + ) + } + + torrentsTable() { + return ( + <div> + <table class="table table-fixed table-hover table-sm table-striped table-hover-purple table-padding"> + <thead> + <tr> + <th class="search-name-col">Name</th> + <th class="text-right">Size</th> + <th class="text-right">Seeds</th> + <th class="text-right d-none d-md-table-cell">Leeches</th> + <th class="text-right d-none d-md-table-cell">Created</th> + <th></th> + </tr> + </thead> + <tbody> + {this.state.results.torrents.map(torrent => ( + <tr> + { !torrent.name ? ( + <td className="path_column"> + <a class="text-body" + href={magnetLink(torrent.infohash, torrent.path, torrent.index_)}> + {getFileName(torrent.path)} + </a> + </td> + ) : ( + <td class="search-name-cell"> + <a class="text-body" + href={magnetLink(torrent.infohash, torrent.name, torrent.index_)}> + {torrent.name} + </a> + </td> + )} + <td class="text-right text-muted">{humanFileSize(torrent.size_bytes, true)}</td> + <td class="text-right text-success"> + <svg class="icon icon-arrow-up d-none d-sm-inline mr-1"><use xlinkHref="#icon-arrow-up"></use></svg> + {torrent.seeders} + </td> + <td class="text-right text-danger d-none d-md-table-cell"> + <svg class="icon icon-arrow-down mr-1"><use xlinkHref="#icon-arrow-down"></use></svg> + {torrent.leechers} + </td> + <td class="text-right text-muted d-none d-md-table-cell" + data-balloon={`Scraped ${moment(torrent.scraped_date * 1000).fromNow()}`} + data-balloon-pos="down"> + {moment(torrent.created_unix * 1000).fromNow()} + </td> + <td class="text-right"> + <a class="btn btn-sm no-outline p-1" + href={magnetLink(torrent.infohash, (torrent.name) ? torrent.name : torrent.path, torrent.index_)} + data-balloon="Magnet link" + data-balloon-pos="left"> + <svg class="icon icon-magnet"><use xlinkHref="#icon-magnet"></use></svg> + </a> + <a class="btn btn-sm no-outline p-1 d-none d-sm-inline" + href={`https://gitlab.com/dessalines/torrents.csv/issues/new?issue[title]=Report%20Torrent%20infohash%20${torrent.infohash}`} + target="_blank" + data-balloon="Report Torrent" + data-balloon-pos="left"> + <svg class="icon icon-flag"><use xlinkHref="#icon-flag"></use></svg> + </a> + </td> + </tr> + ))} + </tbody> + </table> + {this.paginator()} + </div> + ); + } + + paginator() { + return ( + <nav> + <ul class="pagination justify-content-center"> + <li className={(this.state.searchParams.page == 1) ? "page-item disabled" : "page-item"}> + <button class="page-link" + onClick={linkEvent({ i: this, nextPage: false }, this.switchPage)}> + Previous + </button> + </li> + <li class="page-item"> + <button class="page-link" + onClick={linkEvent({ i: this, nextPage: true }, this.switchPage)}> + Next + </button> + </li> + </ul> + </nav> + ); + } + + switchPage(a: { i: Search, nextPage: boolean }, event) { + let newSearch = a.i.state.searchParams; + newSearch.page += (a.nextPage) ? 1 : -1; + a.i.props.history.push(`/search/${newSearch.type_}/${newSearch.q}/${newSearch.page}`); + } +} diff --git a/ui/src/env.ts b/ui/src/env.ts new file mode 100644 index 00000000..a8e72d90 --- /dev/null +++ b/ui/src/env.ts @@ -0,0 +1,3 @@ +// export const endpoint = window.location.origin; +export const endpoint = "http://localhost:8080"; +export let wsUri = (window.location.protocol=='https:') ? 'wss://' : 'ws://' + endpoint.substr(7) + '/service/ws'; diff --git a/ui/src/index.html b/ui/src/index.html new file mode 100644 index 00000000..2a3e4198 --- /dev/null +++ b/ui/src/index.html @@ -0,0 +1,19 @@ +<html lang="en"> + +<head> + <!-- Required meta tags --> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <link rel="shortcut icon" type="image/ico" href="/static/assets/favicon.ico" /> + + <title>rust-reddit-fediverse</title> + <link rel="stylesheet" href="https://bootswatch.com/4/darkly/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/balloon-css/0.5.0/balloon.min.css"> +</head> + +<body> + <div id="app"></div> + $bundles +</body> + +</html> diff --git a/ui/src/index.tsx b/ui/src/index.tsx new file mode 100644 index 00000000..36e5681d --- /dev/null +++ b/ui/src/index.tsx @@ -0,0 +1,42 @@ +import { render, Component } from 'inferno'; +import { HashRouter, Route, Switch } from 'inferno-router'; + +import { Navbar } from './components/navbar'; +import { Home } from './components/home'; +import { Login } from './components/login'; + +import './main.css'; + +import { WebSocketService } from './services'; + +const container = document.getElementById('app'); + +class Index extends Component<any, any> { + + constructor(props, context) { + super(props, context); + WebSocketService.Instance; + } + + render() { + return ( + <HashRouter> + <Navbar /> + <div class="mt-3 p-0"> + <Switch> + <Route exact path="/" component={Home} /> + <Route path={`/login`} component={Login} /> + {/* + <Route path={`/search/:type_/:q/:page`} component={Search} /> + <Route path={`/submit`} component={Submit} /> + <Route path={`/user/:id`} component={Login} /> + <Route path={`/community/:id`} component={Login} /> + */} + </Switch> + </div> + </HashRouter> + ); + } +} + +render(<Index />, container); diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts new file mode 100644 index 00000000..c1550cc1 --- /dev/null +++ b/ui/src/interfaces.ts @@ -0,0 +1,14 @@ +export interface LoginForm { + username: string; + password: string; +} +export interface RegisterForm { + username: string; + email?: string; + password: string; + password_verify: string; +} + +export enum UserOperation { + Login, Register +} diff --git a/ui/src/main.css b/ui/src/main.css new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ui/src/main.css diff --git a/ui/src/services.ts b/ui/src/services.ts new file mode 100644 index 00000000..b9536aed --- /dev/null +++ b/ui/src/services.ts @@ -0,0 +1,57 @@ +import { wsUri } from './env'; +import { LoginForm, RegisterForm, UserOperation } from './interfaces'; + +export class WebSocketService { + private static _instance: WebSocketService; + private _ws; + private conn: WebSocket; + + private constructor() { + console.log("Creating WSS"); + this.connect(); + console.log(wsUri); + } + + public static get Instance(){ + return this._instance || (this._instance = new this()); + } + + private connect() { + this.disconnect(); + this.conn = new WebSocket(wsUri); + console.log('Connecting...'); + this.conn.onopen = (() => { + console.log('Connected.'); + }); + this.conn.onmessage = (e => { + console.log('Received: ' + e.data); + }); + this.conn.onclose = (() => { + console.log('Disconnected.'); + this.conn = null; + }); + } + private disconnect() { + if (this.conn != null) { + console.log('Disconnecting...'); + this.conn.close(); + this.conn = null; + } + } + + public login(loginForm: LoginForm) { + this.conn.send(this.wsSendWrapper(UserOperation.Login, loginForm)); + } + + public register(registerForm: RegisterForm) { + this.conn.send(this.wsSendWrapper(UserOperation.Register, registerForm)); + } + + private wsSendWrapper(op: UserOperation, data: any): string { + let send = { op: UserOperation[op], data: data }; + console.log(send); + return JSON.stringify(send); + } + + +} diff --git a/ui/src/utils.ts b/ui/src/utils.ts new file mode 100644 index 00000000..e141c681 --- /dev/null +++ b/ui/src/utils.ts @@ -0,0 +1,2 @@ +export let repoUrl = 'https://github.com/dessalines/rust-reddit-fediverse'; +export let wsUri = (window.location.protocol=='https:'&&'wss://'||'ws://')+window.location.host + '/service/ws/'; |