diff options
author | Dessalines <tyhou13@gmx.com> | 2019-03-22 18:42:57 -0700 |
---|---|---|
committer | Dessalines <tyhou13@gmx.com> | 2019-03-22 18:42:57 -0700 |
commit | e570c70701e1c66dad12bef76821f6d94c717a02 (patch) | |
tree | 070d5ba322226acd5fe0d0a7757cb936462ab2ff /ui/src | |
parent | 816aa0b15f3766e340d8722f03e8b3a7633ab6fb (diff) |
Adding login and Register
- Login and Register mostly working.
- Starting to work on creating communities.
Diffstat (limited to 'ui/src')
-rw-r--r-- | ui/src/components/create-community.tsx | 90 | ||||
-rw-r--r-- | ui/src/components/create-post.tsx | 57 | ||||
-rw-r--r-- | ui/src/components/login.tsx | 57 | ||||
-rw-r--r-- | ui/src/components/navbar.tsx | 54 | ||||
-rw-r--r-- | ui/src/index.tsx | 7 | ||||
-rw-r--r-- | ui/src/interfaces.ts | 24 | ||||
-rw-r--r-- | ui/src/main.css | 5 | ||||
-rw-r--r-- | ui/src/services.ts | 57 | ||||
-rw-r--r-- | ui/src/services/UserService.ts | 51 | ||||
-rw-r--r-- | ui/src/services/WebSocketService.ts | 37 | ||||
-rw-r--r-- | ui/src/services/index.ts | 2 | ||||
-rw-r--r-- | ui/src/utils.ts | 7 |
12 files changed, 359 insertions, 89 deletions
diff --git a/ui/src/components/create-community.tsx b/ui/src/components/create-community.tsx new file mode 100644 index 00000000..dbacd18d --- /dev/null +++ b/ui/src/components/create-community.tsx @@ -0,0 +1,90 @@ +import { Component, linkEvent } from 'inferno'; +import { Subscription } from "rxjs"; +import { retryWhen, delay, take } from 'rxjs/operators'; +import { CommunityForm, UserOperation } from '../interfaces'; +import { WebSocketService, UserService } from '../services'; +import { msgOp } from '../utils'; + +interface State { + communityForm: CommunityForm; +} + +let emptyState: State = { + communityForm: { + name: null, + } +} + +export class CreateCommunity extends Component<any, State> { + private subscription: Subscription; + + constructor(props, context) { + super(props, context); + + this.state = emptyState; + + this.subscription = WebSocketService.Instance.subject + .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) + .subscribe( + (msg) => this.parseMessage(msg), + (err) => console.error(err), + ); + } + + componentWillUnmount() { + this.subscription.unsubscribe(); + } + + render() { + return ( + <div class="container"> + <div class="row"> + <div class="col-12 col-lg-6 mb-4"> + {this.communityForm()} + </div> + </div> + </div> + ) + } + + communityForm() { + return ( + <div> + <form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}> + <h3>Create Forum</h3> + <div class="form-group row"> + <label class="col-sm-2 col-form-label">Name</label> + <div class="col-sm-10"> + <input type="text" class="form-control" value={this.state.communityForm.name} onInput={linkEvent(this, this.handleCommunityNameChange)} required minLength={3} /> + </div> + </div> + <div class="form-group row"> + <div class="col-sm-10"> + <button type="submit" class="btn btn-secondary">Create</button> + </div> + </div> + </form> + </div> + ); + } + + handleCreateCommunitySubmit(i: CreateCommunity, event) { + event.preventDefault(); + WebSocketService.Instance.createCommunity(i.state.communityForm); + } + + handleCommunityNameChange(i: CreateCommunity, event) { + i.state.communityForm.name = event.target.value; + i.setState(i.state); + } + + parseMessage(msg: any) { + let op: UserOperation = msgOp(msg); + if (msg.error) { + alert(msg.error); + return; + } else { + } + } + +} diff --git a/ui/src/components/create-post.tsx b/ui/src/components/create-post.tsx new file mode 100644 index 00000000..bb6e60e2 --- /dev/null +++ b/ui/src/components/create-post.tsx @@ -0,0 +1,57 @@ +import { Component, linkEvent } from 'inferno'; + +import { LoginForm, PostForm, UserOperation } from '../interfaces'; +import { WebSocketService, UserService } from '../services'; +import { msgOp } from '../utils'; + +interface State { + postForm: PostForm; +} + +let emptyState: State = { + postForm: { + name: null, + url: null, + attributed_to: null + } +} + +export class CreatePost extends Component<any, State> { + + constructor(props, context) { + super(props, context); + + this.state = emptyState; + + WebSocketService.Instance.subject.subscribe( + (msg) => this.parseMessage(msg), + (err) => console.error(err), + () => console.log('complete') + ); + } + + + render() { + return ( + <div class="container"> + <div class="row"> + <div class="col-12 col-lg-6 mb-4"> + create post + {/* {this.postForm()} */} + </div> + </div> + </div> + ) + } + + parseMessage(msg: any) { + console.log(msg); + let op: UserOperation = msgOp(msg); + if (msg.error) { + alert(msg.error); + return; + } else { + } + } + +} diff --git a/ui/src/components/login.tsx b/ui/src/components/login.tsx index fd6f5045..372b1557 100644 --- a/ui/src/components/login.tsx +++ b/ui/src/components/login.tsx @@ -1,7 +1,9 @@ import { Component, linkEvent } from 'inferno'; - -import { LoginForm, RegisterForm } from '../interfaces'; -import { WebSocketService } from '../services'; +import { Subscription } from "rxjs"; +import { retryWhen, delay, take } from 'rxjs/operators'; +import { LoginForm, RegisterForm, UserOperation } from '../interfaces'; +import { WebSocketService, UserService } from '../services'; +import { msgOp } from '../utils'; interface State { loginForm: LoginForm; @@ -10,24 +12,36 @@ interface State { let emptyState: State = { loginForm: { - username: null, - password: null + username_or_email: undefined, + password: undefined }, registerForm: { - username: null, - password: null, - password_verify: null + username: undefined, + password: undefined, + password_verify: undefined } } export class Login extends Component<any, State> { + private subscription: Subscription; constructor(props, context) { super(props, context); this.state = emptyState; + this.subscription = WebSocketService.Instance.subject + .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) + .subscribe( + (msg) => this.parseMessage(msg), + (err) => console.error(err), + ); + } + + componentWillUnmount() { + this.subscription.unsubscribe(); } + render() { return ( <div class="container"> @@ -51,7 +65,7 @@ export class Login extends Component<any, State> { <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} /> + <input type="text" class="form-control" value={this.state.loginForm.username_or_email} onInput={linkEvent(this, this.handleLoginUsernameChange)} required minLength={3} /> </div> </div> <div class="form-group row"> @@ -108,38 +122,55 @@ export class Login extends Component<any, State> { } 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; + i.state.loginForm.username_or_email = event.target.value; + i.setState(i.state); } handleLoginPasswordChange(i: Login, event) { i.state.loginForm.password = event.target.value; + i.setState(i.state); } 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; + i.setState(i.state); } handleRegisterEmailChange(i: Login, event) { i.state.registerForm.email = event.target.value; + i.setState(i.state); } handleRegisterPasswordChange(i: Login, event) { i.state.registerForm.password = event.target.value; + i.setState(i.state); } - + handleRegisterPasswordVerifyChange(i: Login, event) { i.state.registerForm.password_verify = event.target.value; + i.setState(i.state); + } + + parseMessage(msg: any) { + let op: UserOperation = msgOp(msg); + if (msg.error) { + alert(msg.error); + return; + } else { + if (op == UserOperation.Register || op == UserOperation.Login) { + UserService.Instance.login(msg.jwt); + this.props.history.push('/'); + } + } } } diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx index 86d5d1d2..4cf6d6d2 100644 --- a/ui/src/components/navbar.tsx +++ b/ui/src/components/navbar.tsx @@ -1,38 +1,62 @@ import { Component, linkEvent } from 'inferno'; import { Link } from 'inferno-router'; import { repoUrl } from '../utils'; +import { UserService } from '../services'; export class Navbar extends Component<any, any> { constructor(props, context) { super(props, context); + this.state = {isLoggedIn: UserService.Instance.loggedIn}; + + // Subscribe to user changes + UserService.Instance.sub.subscribe(user => { + let loggedIn: boolean = user !== null; + this.setState({isLoggedIn: loggedIn}); + }); } render() { return ( - <div class="sticky-top">{this.navbar()}</div> + <div>{this.navbar()}</div> ) } // TODO class active corresponding to current page + // TODO toggle css collapse 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 class="navbar navbar-expand-sm navbar-light bg-light p-0 px-3 shadow"> + <a class="navbar-brand" href="#">rrf</a> + <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse navbar-collapse"> + <ul class="navbar-nav mr-auto"> + <li class="nav-item"> + <a class="nav-link" href={repoUrl}>github</a> + </li> + <li class="nav-item"> + <Link class="nav-link" to="/create_post">Create Post</Link> + </li> + <li class="nav-item"> + <Link class="nav-link" to="/create_community">Create Forum</Link> + </li> + </ul> + <ul class="navbar-nav ml-auto mr-2"> + <li class="nav-item"> + {this.state.isLoggedIn ? + <a role="button" class="nav-link pointer" onClick={ linkEvent(this, this.handleLogoutClick) }>Logout</a> : + <Link class="nav-link" to="/login">Login</Link> + } + </li> + </ul> + </div> </nav> ); } + handleLogoutClick(i: Navbar, event) { + UserService.Instance.logout(); + } } diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 36e5681d..1eb3a7d5 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -4,10 +4,12 @@ import { HashRouter, Route, Switch } from 'inferno-router'; import { Navbar } from './components/navbar'; import { Home } from './components/home'; import { Login } from './components/login'; +import { CreatePost } from './components/create-post'; +import { CreateCommunity } from './components/create-community'; import './main.css'; -import { WebSocketService } from './services'; +import { WebSocketService, UserService } from './services'; const container = document.getElementById('app'); @@ -16,6 +18,7 @@ class Index extends Component<any, any> { constructor(props, context) { super(props, context); WebSocketService.Instance; + UserService.Instance; } render() { @@ -26,6 +29,8 @@ class Index extends Component<any, any> { <Switch> <Route exact path="/" component={Home} /> <Route path={`/login`} component={Login} /> + <Route path={`/create_post`} component={CreatePost} /> + <Route path={`/create_community`} component={CreateCommunity} /> {/* <Route path={`/search/:type_/:q/:page`} component={Search} /> <Route path={`/submit`} component={Submit} /> diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index c1550cc1..da5c415b 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -1,7 +1,17 @@ -export interface LoginForm { +export enum UserOperation { + Login, Register, CreateCommunity +} + +export interface User { + id: number username: string; +} + +export interface LoginForm { + username_or_email: string; password: string; } + export interface RegisterForm { username: string; email?: string; @@ -9,6 +19,14 @@ export interface RegisterForm { password_verify: string; } -export enum UserOperation { - Login, Register +export interface CommunityForm { + name: string; + updated?: number +} + +export interface PostForm { + name: string; + url: string; + attributed_to: string; + updated?: number } diff --git a/ui/src/main.css b/ui/src/main.css index e69de29b..77e093e8 100644 --- a/ui/src/main.css +++ b/ui/src/main.css @@ -0,0 +1,5 @@ + + +.pointer { + cursor: pointer; +} diff --git a/ui/src/services.ts b/ui/src/services.ts deleted file mode 100644 index b9536aed..00000000 --- a/ui/src/services.ts +++ /dev/null @@ -1,57 +0,0 @@ -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/services/UserService.ts b/ui/src/services/UserService.ts new file mode 100644 index 00000000..af0d1d15 --- /dev/null +++ b/ui/src/services/UserService.ts @@ -0,0 +1,51 @@ +import * as Cookies from 'js-cookie'; +import { User } from '../interfaces'; +import * as jwt_decode from 'jwt-decode'; +import { Subject } from 'rxjs'; + +export class UserService { + private static _instance: UserService; + private user: User; + public sub: Subject<User> = new Subject<User>(); + + private constructor() { + let jwt = Cookies.get("jwt"); + if (jwt) { + this.setUser(jwt); + } else { + console.log('No JWT cookie found.'); + } + + } + + public login(jwt: string) { + Cookies.set("jwt", jwt); + console.log("jwt cookie set"); + this.setUser(jwt); + } + + public logout() { + this.user = null; + Cookies.remove("jwt"); + console.log("Logged out."); + this.sub.next(null); + } + + public get loggedIn(): boolean { + return this.user !== undefined; + } + + public get auth(): string { + return Cookies.get("jwt"); + } + + private setUser(jwt: string) { + this.user = jwt_decode(jwt); + this.sub.next(this.user); + console.log(this.user.username); + } + + public static get Instance(){ + return this._instance || (this._instance = new this()); + } +} diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts new file mode 100644 index 00000000..1882b125 --- /dev/null +++ b/ui/src/services/WebSocketService.ts @@ -0,0 +1,37 @@ +import { wsUri } from '../env'; +import { LoginForm, RegisterForm, UserOperation, CommunityForm } from '../interfaces'; +import { webSocket } from 'rxjs/webSocket'; +import { Subject } from 'rxjs'; +import { UserService } from './'; + +export class WebSocketService { + private static _instance: WebSocketService; + public subject: Subject<{}>; + + private constructor() { + this.subject = webSocket(wsUri); + console.log(`Connected to ${wsUri}`); + } + + public static get Instance(){ + return this._instance || (this._instance = new this()); + } + + public login(loginForm: LoginForm) { + this.subject.next(this.wsSendWrapper(UserOperation.Login, loginForm)); + } + + public register(registerForm: RegisterForm) { + this.subject.next(this.wsSendWrapper(UserOperation.Register, registerForm)); + } + + public createCommunity(communityForm: CommunityForm) { + this.subject.next(this.wsSendWrapper(UserOperation.CreateCommunity, communityForm, UserService.Instance.auth)); + } + + private wsSendWrapper(op: UserOperation, data: any, auth?: string) { + let send = { op: UserOperation[op], data: data, auth: auth }; + console.log(send); + return send; + } +} diff --git a/ui/src/services/index.ts b/ui/src/services/index.ts new file mode 100644 index 00000000..f0f4ccf5 --- /dev/null +++ b/ui/src/services/index.ts @@ -0,0 +1,2 @@ +export { UserService } from './UserService'; +export { WebSocketService } from './WebSocketService'; diff --git a/ui/src/utils.ts b/ui/src/utils.ts index e141c681..d3d9696e 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -1,2 +1,9 @@ +import { UserOperation } from './interfaces'; + export let repoUrl = 'https://github.com/dessalines/rust-reddit-fediverse'; export let wsUri = (window.location.protocol=='https:'&&'wss://'||'ws://')+window.location.host + '/service/ws/'; + +export function msgOp(msg: any): UserOperation { + let opStr: string = msg.op; + return UserOperation[opStr]; +} |