mirror of
https://github.com/mgerb/classic-wow-forums
synced 2026-01-11 01:22:49 +00:00
client/server - update reply
This commit is contained in:
@@ -25,7 +25,7 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
height: 25px;
|
height: 30px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import './editor.scss';
|
|||||||
interface Props {
|
interface Props {
|
||||||
categoryId?: string;
|
categoryId?: string;
|
||||||
onClose: (cancel: boolean) => any;
|
onClose: (cancel: boolean) => any;
|
||||||
|
editingReply?: ReplyModel;
|
||||||
quotedReply?: ReplyModel;
|
quotedReply?: ReplyModel;
|
||||||
threadId?: string;
|
threadId?: string;
|
||||||
}
|
}
|
||||||
@@ -40,6 +41,11 @@ export class Editor extends React.Component<Props, State> {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
// set content if user is editing a reply
|
||||||
|
if (this.props.editingReply) {
|
||||||
|
this.onContentChange({ target: { value: this.props.editingReply.content } });
|
||||||
|
}
|
||||||
|
|
||||||
if (this.titleRef) {
|
if (this.titleRef) {
|
||||||
this.titleRef.focus();
|
this.titleRef.focus();
|
||||||
} else {
|
} else {
|
||||||
@@ -84,12 +90,13 @@ export class Editor extends React.Component<Props, State> {
|
|||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
content,
|
content,
|
||||||
|
id: get(this.props, 'editingReply.id'),
|
||||||
thread_id: this.props.threadId,
|
thread_id: this.props.threadId,
|
||||||
quote_id: get(this.props, 'quotedReply.id') || undefined,
|
quote_id: get(this.props, 'quotedReply.id') || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.post('/api/reply', data);
|
this.props.editingReply ? await axios.put('/api/reply', data) : await axios.post('/api/reply', data);
|
||||||
this.props.onClose(false);
|
this.props.onClose(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.setState({ errorMessage: 'Server error. Please try again later.' });
|
this.setState({ errorMessage: 'Server error. Please try again later.' });
|
||||||
@@ -140,17 +147,25 @@ export class Editor extends React.Component<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEditorTitle(): string {
|
||||||
|
if (!this.props.threadId) {
|
||||||
|
return 'New Topic';
|
||||||
|
}
|
||||||
|
return `${this.props.editingReply ? 'Edit' : 'New'} Reply`;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="editor-background">
|
<div className="editor-background">
|
||||||
<ContentContainer className="editor-container">
|
<ContentContainer className="editor-container">
|
||||||
<form className="editor" onSubmit={event => this.onSubmit(event)} onReset={() => this.props.onClose(true)}>
|
<form className="editor" onSubmit={event => this.onSubmit(event)} onReset={() => this.props.onClose(true)}>
|
||||||
|
|
||||||
<h2 style={{ color: 'white' }}>New {this.props.threadId ? 'Reply' : 'Topic'}</h2>
|
<h2 style={{ color: 'white' }}>{this.getEditorTitle()}</h2>
|
||||||
{!this.props.threadId && this.renderTopicInput()}
|
{!this.props.threadId && this.renderTopicInput()}
|
||||||
|
|
||||||
<div><label>Content</label></div>
|
<div><label>Content</label></div>
|
||||||
<textarea className="input editor__text-area flex-1"
|
<textarea className="input editor__text-area flex-1"
|
||||||
|
value={this.state.content}
|
||||||
onChange={event => this.onContentChange(event)} maxLength={2000}
|
onChange={event => this.onContentChange(event)} maxLength={2000}
|
||||||
ref={ref => this.contentRef = ref}
|
ref={ref => this.contentRef = ref}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { ThreadService } from '../../services';
|
|||||||
import { Editor, 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 { UserStore } from '../../stores/user-store';
|
||||||
import './forum.scss';
|
|
||||||
import { Oauth } from '../../util';
|
import { Oauth } from '../../util';
|
||||||
|
import './forum.scss';
|
||||||
|
|
||||||
const stickyImage = require('../../assets/sticky.gif');
|
const stickyImage = require('../../assets/sticky.gif');
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ export class Forum extends React.Component<Props, State> {
|
|||||||
|
|
||||||
renderCell(content: JSX.Element | string, style: any, center?: boolean, header?: boolean) {
|
renderCell(content: JSX.Element | string, style: any, center?: boolean, header?: boolean) {
|
||||||
let classNames: string = '';
|
let classNames: string = '';
|
||||||
classNames += center ? ' forum-cell--center': '';
|
classNames += center ? ' forum-cell--center' : '';
|
||||||
classNames += header ? ' forum-cell--header' : ' forum-cell--body';
|
classNames += header ? ' forum-cell--header' : ' forum-cell--body';
|
||||||
return <div className={`forum-cell flex-1 ${classNames}`} style={style}>{content}</div>;
|
return <div className={`forum-cell flex-1 ${classNames}`} style={style}>{content}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import { get, find, map } from 'lodash';
|
import { get, find, orderBy } from 'lodash';
|
||||||
import marked from 'marked';
|
import marked from 'marked';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { inject, observer } from 'mobx-react';
|
||||||
import { CharacterService, ThreadService } from '../../services';
|
import { CharacterService, ThreadService } from '../../services';
|
||||||
import { Editor, Portrait, ScrollToTop } from '../../components';
|
import { Editor, Portrait, ScrollToTop } from '../../components';
|
||||||
import { ReplyModel, ThreadModel } from '../../model';
|
import { ReplyModel, ThreadModel } from '../../model';
|
||||||
|
import { UserStore } from '../../stores/user-store';
|
||||||
|
import { Oauth } from '../../util';
|
||||||
import './thread.scss';
|
import './thread.scss';
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<any> {}
|
interface Props extends RouteComponentProps<any> {
|
||||||
|
userStore: UserStore;
|
||||||
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
editingReply?: ReplyModel;
|
||||||
quotedReply?: ReplyModel;
|
quotedReply?: ReplyModel;
|
||||||
showEditor: boolean;
|
showEditor: boolean;
|
||||||
thread?: ThreadModel;
|
thread?: ThreadModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@inject('userStore')
|
||||||
|
@observer
|
||||||
export class Thread extends React.Component<Props, State> {
|
export class Thread extends React.Component<Props, State> {
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
@@ -31,20 +39,27 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
|
|
||||||
private async getReplies() {
|
private async getReplies() {
|
||||||
const thread = await ThreadService.getThread(this.props.match.params['threadId']);
|
const thread = await ThreadService.getThread(this.props.match.params['threadId']);
|
||||||
thread.replies = map(thread.replies, (reply) => { // add the thread topic to the front of the list
|
thread.replies = orderBy(thread.replies, ['inserted_at']);
|
||||||
return reply;
|
|
||||||
});
|
|
||||||
this.setState({ thread });
|
this.setState({ thread });
|
||||||
}
|
}
|
||||||
|
|
||||||
private onReplyClick() {
|
private onReplyClick() {
|
||||||
this.setState({ showEditor: true });
|
this.props.userStore.user ? this.setState({ showEditor: true }) : Oauth.openOuathWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
private onQuoteClick(reply: ReplyModel) {
|
private onQuoteClick(reply: ReplyModel) {
|
||||||
|
this.props.userStore.user ?
|
||||||
this.setState({
|
this.setState({
|
||||||
showEditor: true,
|
showEditor: true,
|
||||||
quotedReply: reply,
|
quotedReply: reply,
|
||||||
|
}) :
|
||||||
|
Oauth.openOuathWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onEditClick(reply: ReplyModel) {
|
||||||
|
this.setState({
|
||||||
|
editingReply: reply,
|
||||||
|
showEditor: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +67,7 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
showEditor: false,
|
showEditor: false,
|
||||||
quotedReply: undefined,
|
quotedReply: undefined,
|
||||||
|
editingReply: undefined,
|
||||||
});
|
});
|
||||||
if (!cancel) {
|
if (!cancel) {
|
||||||
this.getReplies();
|
this.getReplies();
|
||||||
@@ -99,6 +115,12 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderEditbutton(reply: ReplyModel): any {
|
||||||
|
if (get(this.props, 'userStore.user.id') === reply.user_id) {
|
||||||
|
return <a style={{ paddingRight: '10px' }} onClick={() => this.onEditClick(reply)}>Edit</a>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderReplies(): any {
|
renderReplies(): any {
|
||||||
return this.state.thread!.replies.map((reply, index) => {
|
return this.state.thread!.replies.map((reply, index) => {
|
||||||
const replyDark = index % 2 === 0 ? 'reply--dark' : '';
|
const replyDark = index % 2 === 0 ? 'reply--dark' : '';
|
||||||
@@ -114,7 +136,8 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
<b>{`${index + 1}. `}{index > 0 && 'Re: '}{this.state.thread!.title}</b>
|
<b>{`${index + 1}. `}{index > 0 && 'Re: '}{this.state.thread!.title}</b>
|
||||||
<small style={{ paddingLeft: '5px' }}>| {this.getTimeFormat(reply.inserted_at)}</small>
|
<small style={{ paddingLeft: '5px' }}>| {this.getTimeFormat(reply.inserted_at)}</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex--center">
|
||||||
|
{this.renderEditbutton(reply)}
|
||||||
<img src={require('../../assets/quote-button.gif')}
|
<img src={require('../../assets/quote-button.gif')}
|
||||||
className="reply__title__button"
|
className="reply__title__button"
|
||||||
onClick={() => this.onQuoteClick(reply)}/>
|
onClick={() => this.onQuoteClick(reply)}/>
|
||||||
@@ -127,6 +150,7 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
<div className="reply__content markdown-container">
|
<div className="reply__content markdown-container">
|
||||||
{this.renderQuotedReply(reply)}
|
{this.renderQuotedReply(reply)}
|
||||||
<div className={bluePost} dangerouslySetInnerHTML={{ __html: marked(reply.content, { sanitize: true }) }}/>
|
<div className={bluePost} dangerouslySetInnerHTML={{ __html: marked(reply.content, { sanitize: true }) }}/>
|
||||||
|
{reply.edited && <small className="red">[ post edited by {reply.user.character_name || reply.user.battletag} ]</small>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -150,6 +174,7 @@ export class Thread extends React.Component<Props, State> {
|
|||||||
<Editor threadId={this.props.match.params['threadId']}
|
<Editor threadId={this.props.match.params['threadId']}
|
||||||
onClose={cancel => this.onEditorClose(cancel)}
|
onClose={cancel => this.onEditorClose(cancel)}
|
||||||
quotedReply={this.state.quotedReply}
|
quotedReply={this.state.quotedReply}
|
||||||
|
editingReply={this.state.editingReply}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<div className="topic-bg">
|
<div className="topic-bg">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>Classic WoW Forums</title>
|
<title>Classic WoW Forums</title>
|
||||||
<link href="https://fonts.googleapis.com/css?family=Roboto:100" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=Roboto:100" rel="stylesheet">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ defmodule MyApp.Data.Reply do
|
|||||||
|
|
||||||
case Repo.update_all(query, []) do
|
case Repo.update_all(query, []) do
|
||||||
nil -> {:error, "update thread error"}
|
nil -> {:error, "update thread error"}
|
||||||
_ -> {:ok, reply}
|
{1, _} -> {:ok, reply}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -58,20 +58,20 @@ defmodule MyApp.Data.Reply do
|
|||||||
def user_update(params) do
|
def user_update(params) do
|
||||||
id = Map.get(params, "id")
|
id = Map.get(params, "id")
|
||||||
user_id = Map.get(params, "user_id")
|
user_id = Map.get(params, "user_id")
|
||||||
|
content = Map.get(params, "content")
|
||||||
|
|
||||||
if is_nil(id) || is_nil(user_id) do
|
if is_nil(id) || is_nil(user_id) || is_nil(content) do
|
||||||
{:error, "Invalid reply"}
|
{:error, "Invalid reply"}
|
||||||
else
|
else
|
||||||
Repo.get_by(Data.Reply, %{id: id, user_id: user_id})
|
query = from r in Data.Reply,
|
||||||
|> process_user_update(params)
|
where: r.id == ^id and r.user_id == ^user_id,
|
||||||
end
|
update: [set: [content: ^content, edited: true]]
|
||||||
end
|
|
||||||
|
|
||||||
defp process_user_update(reply, _params) when is_nil(reply), do: {:error, "Invalid reply"}
|
case Repo.update_all(query, []) do
|
||||||
defp process_user_update(reply, params) when not is_nil(reply) do
|
nil -> {:error, "update reply error"}
|
||||||
user_update_changeset(reply, params)
|
{1, _} -> {:ok, "ok"}
|
||||||
|> Repo.update
|
end
|
||||||
|> Data.Util.process_insert_or_update
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user