mirror of
https://github.com/mgerb/classic-wow-forums
synced 2026-01-10 09:02:50 +00:00
client - wip new topic
This commit is contained in:
47
client/app/components/editor/editor.scss
Normal file
47
client/app/components/editor/editor.scss
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
.editor-container {
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: calc(100% - 96px);
|
||||||
|
width: 100%;
|
||||||
|
position: fixed;
|
||||||
|
background-color: rgba(0, 0, 0, .5);
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
height: 80%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
height: 25px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text-area {
|
||||||
|
min-height: 100px;
|
||||||
|
resize: none;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__preview {
|
||||||
|
min-height: 100px;
|
||||||
|
background-color: #161616;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error-message {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
88
client/app/components/editor/editor.tsx
Normal file
88
client/app/components/editor/editor.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import marked from 'marked';
|
||||||
|
import axios from '../../axios/axios';
|
||||||
|
import { ContentContainer } from '../content-container/content-container';
|
||||||
|
import './editor.scss';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
categoryId: string;
|
||||||
|
onClose: (cancel: boolean) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
contentPreview: string;
|
||||||
|
characterCount: number;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Editor extends React.Component<Props, State> {
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
contentPreview: '',
|
||||||
|
characterCount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onContentChange(event: any) {
|
||||||
|
this.setState({
|
||||||
|
content: event.target.value,
|
||||||
|
contentPreview: marked(event.target.value, { sanitize: true }),
|
||||||
|
characterCount: event.target.value.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
|
||||||
|
const { title, content } = this.state;
|
||||||
|
|
||||||
|
if (title === '' || content === '') {
|
||||||
|
this.setState({ errorMessage: 'One of your inputs is blank.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
content,
|
||||||
|
title,
|
||||||
|
category_id: this.props.categoryId,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.newThread(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async newThread(data: any) {
|
||||||
|
try {
|
||||||
|
await axios.post('/api/thread', data);
|
||||||
|
this.props.onClose(false);
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({ errorMessage: 'Server error. Please try again later.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="editor-container">
|
||||||
|
<ContentContainer className="editor">
|
||||||
|
<h2 style={{ color: 'white' }}>New Topic</h2>
|
||||||
|
<label>Title</label>
|
||||||
|
<input type="text" className="input editor__title" onChange={event => this.setState({ title: event.target.value })}/>
|
||||||
|
<label>Content</label>
|
||||||
|
<textarea className="input editor__text-area flex-1" onChange={event => this.onContentChange(event)}/>
|
||||||
|
<label>Content Preview</label>
|
||||||
|
<div className="editor__preview flex-1" dangerouslySetInnerHTML={{ __html: this.state.contentPreview }}></div>
|
||||||
|
<div className="editor__submit">
|
||||||
|
<a onClick={() => this.onSubmit()}>Submit</a>
|
||||||
|
<a onClick={() => this.props.onClose(true)} style={{ marginLeft: '10px' }}>Cancel</a>
|
||||||
|
<span className="editor__error-message">{this.state.errorMessage}</span>
|
||||||
|
<span style={{ float: 'right' }}>{this.state.characterCount}/2000</span>
|
||||||
|
</div>
|
||||||
|
</ContentContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './content-container/content-container';
|
export * from './content-container/content-container';
|
||||||
|
export * from './editor/editor';
|
||||||
export * from './footer/footer';
|
export * from './footer/footer';
|
||||||
export * from './forum-nav/forum-nav';
|
export * from './forum-nav/forum-nav';
|
||||||
export * from './header/header';
|
export * from './header/header';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { inject, observer } from 'mobx-react';
|
|||||||
import { Portrait } from '../portrait/portrait';
|
import { Portrait } from '../portrait/portrait';
|
||||||
import { UserStore } from '../../stores/user-store';
|
import { UserStore } from '../../stores/user-store';
|
||||||
import { CharacterService } from '../../services';
|
import { CharacterService } from '../../services';
|
||||||
|
import { Oauth } from '../../util';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -12,18 +13,12 @@ interface Props {
|
|||||||
|
|
||||||
interface State {}
|
interface State {}
|
||||||
|
|
||||||
// TODO: add prod url
|
|
||||||
const oauthUrl: string =
|
|
||||||
process.env.NODE_ENV === 'production'
|
|
||||||
? ''
|
|
||||||
: 'https://us.battle.net/oauth/authorize?redirect_uri=https://localhost/oauth&scope=wow.profile&client_id=2pfsnmd57svcpr5c93k7zb5zrug29xvp&response_type=code';
|
|
||||||
|
|
||||||
@inject('userStore')
|
@inject('userStore')
|
||||||
@observer
|
@observer
|
||||||
export class LoginButton extends React.Component<Props, State> {
|
export class LoginButton extends React.Component<Props, State> {
|
||||||
|
|
||||||
login() {
|
login() {
|
||||||
window.open(oauthUrl, '_blank', 'resizeable=yes, height=900, width=1200');
|
Oauth.openOuathWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPortrait() {
|
renderPortrait() {
|
||||||
|
|||||||
@@ -1,21 +1,30 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
|
import { inject, observer } from 'mobx-react';
|
||||||
import { ThreadService } from '../../services';
|
import { ThreadService } from '../../services';
|
||||||
import { ForumNav, LoginButton, ScrollToTop } from '../../components';
|
import { Editor, ForumNav, LoginButton, ScrollToTop } from '../../components';
|
||||||
import { ThreadModel } from '../../model';
|
import { ThreadModel } from '../../model';
|
||||||
|
import { UserStore } from '../../stores/user-store';
|
||||||
import './forum.scss';
|
import './forum.scss';
|
||||||
|
import { Oauth } from '../../util';
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<any> {}
|
interface Props extends RouteComponentProps<any> {
|
||||||
|
userStore: UserStore;
|
||||||
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
showEditor: boolean;
|
||||||
threads: ThreadModel[];
|
threads: ThreadModel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@inject('userStore')
|
||||||
|
@observer
|
||||||
export class Forum extends React.Component<Props, State> {
|
export class Forum extends React.Component<Props, State> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
showEditor: false,
|
||||||
threads: [],
|
threads: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -36,6 +45,21 @@ export class Forum extends React.Component<Props, State> {
|
|||||||
this.setState({ threads });
|
this.setState({ threads });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onNewTopic() {
|
||||||
|
if (this.props.userStore.user) {
|
||||||
|
this.setState({ showEditor: true });
|
||||||
|
} else {
|
||||||
|
Oauth.openOuathWindow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNewTopicClose(cancel: boolean) {
|
||||||
|
this.setState({ showEditor: false });
|
||||||
|
if (!cancel) {
|
||||||
|
this.getThreads(this.props.match.params['id']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderHeader() {
|
renderHeader() {
|
||||||
return (
|
return (
|
||||||
<div className="forum-header">
|
<div className="forum-header">
|
||||||
@@ -52,13 +76,15 @@ export class Forum extends React.Component<Props, State> {
|
|||||||
<div className="forum-body">
|
<div className="forum-body">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<img src={require('../../assets/forum-menu-left.gif')}/>
|
<img src={require('../../assets/forum-menu-left.gif')}/>
|
||||||
<img src={require('../../assets/forum-menu-newtopic.gif')}/>
|
<img src={require('../../assets/forum-menu-newtopic.gif')}
|
||||||
|
className="clickable"
|
||||||
|
onClick={() => this.onNewTopic()}/>
|
||||||
<img src={require('../../assets/forum-menu-right.gif')}/>
|
<img src={require('../../assets/forum-menu-right.gif')}/>
|
||||||
<img src={require('../../assets/forum-menu-search-left.gif')}/>
|
<img src={require('../../assets/forum-menu-search-left.gif')}/>
|
||||||
<div className="forum-menu-search-bg">
|
<div className="forum-menu-search-bg">
|
||||||
<input name="SearchText"/>
|
<input name="SearchText"/>
|
||||||
</div>
|
</div>
|
||||||
<img src={require('../../assets/forum-menu-search.gif')}/>
|
<img src={require('../../assets/forum-menu-search.gif')} className="clickable"/>
|
||||||
<div className="forumliner-bg"/>
|
<div className="forumliner-bg"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -119,6 +145,8 @@ export class Forum extends React.Component<Props, State> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<ScrollToTop>
|
<ScrollToTop>
|
||||||
|
{this.state.showEditor && <Editor categoryId={this.props.match.params['id']}
|
||||||
|
onClose={cancel => this.onNewTopicClose(cancel)}/>}
|
||||||
{this.renderHeader()}
|
{this.renderHeader()}
|
||||||
{this.renderBody()}
|
{this.renderBody()}
|
||||||
</ScrollToTop>
|
</ScrollToTop>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-top: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.threadTopic-container {
|
.threadTopic-container {
|
||||||
@@ -77,6 +78,8 @@
|
|||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import { get } from 'lodash';
|
import { get, map } from 'lodash';
|
||||||
|
import marked from 'marked';
|
||||||
import { ThreadService } from '../../services';
|
import { ThreadService } from '../../services';
|
||||||
import { ForumNav, Portrait, ScrollToTop } from '../../components';
|
import { Portrait, ScrollToTop } from '../../components';
|
||||||
import { ThreadModel } from '../../model';
|
import { ThreadModel } from '../../model';
|
||||||
import './thread.scss';
|
import './thread.scss';
|
||||||
|
|
||||||
@@ -25,7 +26,10 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
|
|
||||||
private async getThreads() {
|
private async getThreads() {
|
||||||
const thread = await ThreadService.getThread(this.props.match.params['threadId']);
|
const thread = await ThreadService.getThread(this.props.match.params['threadId']);
|
||||||
thread.replies = [thread as any, ...thread.replies]; // add the thread topic to the front of the list
|
thread.replies = map([thread as any, ...thread.replies], (reply) => { // add the thread topic to the front of the list
|
||||||
|
reply.content = marked(reply.content, { sanitize: true });
|
||||||
|
return reply;
|
||||||
|
});
|
||||||
this.setState({ thread });
|
this.setState({ thread });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +54,7 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
<img src={require('../../assets/reply-button.gif')} className="reply__title__button"/>
|
<img src={require('../../assets/reply-button.gif')} className="reply__title__button"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* TODO: xss sanitization */}
|
<div className="reply__content" dangerouslySetInnerHTML={{ __html: reply.content }}/>
|
||||||
<div className="reply__content">{reply.content}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,11 +73,6 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<ScrollToTop {...this.props}>
|
<ScrollToTop {...this.props}>
|
||||||
|
|
||||||
<div style={{ padding: '16px 0 12px 0' }}>
|
|
||||||
{/* todo: */}
|
|
||||||
<ForumNav categoryId={parseInt(this.props.match.params['categoryId'], 10)} {...this.props}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="topic-bg">
|
<div className="topic-bg">
|
||||||
<div className="threadTopic-container">
|
<div className="threadTopic-container">
|
||||||
<div className="threadTopic">
|
<div className="threadTopic">
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
$fontPrimary: #cccccc;
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: Arial,Helvetica,Sans-Serif;
|
font-family: Arial,Helvetica,Sans-Serif;
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
color: #cccccc;
|
color: $fontPrimary;
|
||||||
}
|
}
|
||||||
|
|
||||||
b {
|
b {
|
||||||
@@ -71,3 +73,20 @@ span.grey {
|
|||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
border: 1px solid lighten(#161616, 10%);
|
||||||
|
background-color: #161616;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: $fontPrimary;
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px 10px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
1
client/app/util/index.ts
Normal file
1
client/app/util/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './oauth/oauth';
|
||||||
13
client/app/util/oauth/oauth.ts
Normal file
13
client/app/util/oauth/oauth.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// TODO: add prod url
|
||||||
|
const oauthUrl: string =
|
||||||
|
process.env.NODE_ENV === 'production'
|
||||||
|
? ''
|
||||||
|
: 'https://us.battle.net/oauth/authorize?redirect_uri=https://localhost/oauth&scope=wow.profile&client_id=2pfsnmd57svcpr5c93k7zb5zrug29xvp&response_type=code';
|
||||||
|
|
||||||
|
const openOuathWindow = () => {
|
||||||
|
window.open(oauthUrl, '_blank', 'resizeable=yes, height=900, width=1200');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Oauth = {
|
||||||
|
openOuathWindow,
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/fingerprintjs2": "^1.5.1",
|
"@types/fingerprintjs2": "^1.5.1",
|
||||||
"@types/lodash": "^4.14.92",
|
"@types/lodash": "^4.14.92",
|
||||||
|
"@types/marked": "^0.3.0",
|
||||||
"@types/node": "^9.3.0",
|
"@types/node": "^9.3.0",
|
||||||
"@types/query-string": "^5.0.1",
|
"@types/query-string": "^5.0.1",
|
||||||
"@types/react": "^16.0.34",
|
"@types/react": "^16.0.34",
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"html-webpack-plugin": "^2.30.1",
|
"html-webpack-plugin": "^2.30.1",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
|
"marked": "^0.3.12",
|
||||||
"mobx": "^3.4.1",
|
"mobx": "^3.4.1",
|
||||||
"mobx-react": "^4.3.5",
|
"mobx-react": "^4.3.5",
|
||||||
"node-sass": "^4.7.2",
|
"node-sass": "^4.7.2",
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
version "4.14.92"
|
version "4.14.92"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.92.tgz#6e3cb0b71a1e12180a47a42a744e856c3ae99a57"
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.92.tgz#6e3cb0b71a1e12180a47a42a744e856c3ae99a57"
|
||||||
|
|
||||||
|
"@types/marked@^0.3.0":
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.3.0.tgz#583c223dd33385a1dda01aaf77b0cd0411c4b524"
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "8.5.5"
|
version "8.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.5.5.tgz#6f9e8164ae1a55a9beb1d2571cfb7acf9d720c61"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.5.5.tgz#6f9e8164ae1a55a9beb1d2571cfb7acf9d720c61"
|
||||||
@@ -3675,6 +3679,10 @@ map-obj@^1.0.0, map-obj@^1.0.1:
|
|||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
|
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
|
||||||
|
|
||||||
|
marked@^0.3.12:
|
||||||
|
version "0.3.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.12.tgz#7cf25ff2252632f3fe2406bde258e94eee927519"
|
||||||
|
|
||||||
math-expression-evaluator@^1.2.14:
|
math-expression-evaluator@^1.2.14:
|
||||||
version "1.2.17"
|
version "1.2.17"
|
||||||
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"
|
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ defmodule MyApp.Repo.Migrations.CreateThread do
|
|||||||
create table(:thread) do
|
create table(:thread) do
|
||||||
add :title, :string
|
add :title, :string
|
||||||
add :category_id, :integer
|
add :category_id, :integer
|
||||||
add :content, :string
|
add :content, :string, size: 2000
|
||||||
add :view_count, :integer
|
add :view_count, :integer
|
||||||
add :user_id, references(:user)
|
add :user_id, references(:user)
|
||||||
add :last_reply_id, :integer # TODO: figure this out
|
add :last_reply_id, :integer # TODO: figure this out
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ defmodule MyApp.Repo.Migrations.CreateReply do
|
|||||||
create table(:reply) do
|
create table(:reply) do
|
||||||
add :user_id, references(:user)
|
add :user_id, references(:user)
|
||||||
add :thread_id, references(:thread)
|
add :thread_id, references(:thread)
|
||||||
add :content, :string
|
add :content, :string, size: 2000
|
||||||
add :edited, :boolean
|
add :edited, :boolean
|
||||||
add :quote, :boolean
|
add :quote, :boolean
|
||||||
timestamps()
|
timestamps()
|
||||||
|
|||||||
Reference in New Issue
Block a user