1
0
mirror of https://github.com/mgerb/classic-wow-forums synced 2026-01-10 09:02:50 +00:00

client - admin update thread/reply for locked/hidden/sticky

This commit is contained in:
2018-01-25 22:43:56 -06:00
parent 605e4ba94b
commit ce151bc0c0
8 changed files with 171 additions and 86 deletions

View File

@@ -3,6 +3,7 @@ import { UserModel } from './user';
export interface ReplyModel { export interface ReplyModel {
content: string; content: string;
edited: boolean; edited: boolean;
hidden: boolean;
id: number; id: number;
index?: number; index?: number;
inserted_at: string; inserted_at: string;

View File

@@ -5,6 +5,7 @@ export interface ThreadModel {
category_id: number; category_id: number;
title: string; title: string;
edited: boolean; edited: boolean;
hidden: boolean;
id: number; id: number;
inserted_at: string; inserted_at: string;
last_reply: UserModel; last_reply: UserModel;

View File

@@ -8,11 +8,6 @@ $grey2: #161616;
justify-content: space-between; justify-content: space-between;
} }
.forum-body {
min-height: 500px;
margin-bottom: 100px;
}
.forum-menu-search-bg { .forum-menu-search-bg {
background-image: url('../../assets/forum-menu-search-bg.gif'); background-image: url('../../assets/forum-menu-search-bg.gif');
@@ -36,6 +31,7 @@ $grey2: #161616;
.forum-table { .forum-table {
width: 100%; width: 100%;
border-spacing: 0;
border: 1px solid; border: 1px solid;
border-color: #575757; border-color: #575757;
color: #E2D9B0; color: #E2D9B0;
@@ -46,47 +42,69 @@ $grey2: #161616;
} }
} }
.forum-table__header {
background-image: url('../../assets/thread-topic-bg2.gif') !important;
background-repeat: repeat-x !important;
}
.forum-row { .forum-row {
display: flex;
align-items: center;
height: 24px;
background: $grey1; background: $grey1;
&--header { &__body {
background-image: url('../../assets/thread-topic-bg2.gif'); height: 24px;
background-repeat: repeat-x;
} }
&--dark { &:nth-child(even) {
background: $grey2; background: $grey2;
} }
} }
.forum-cell { .forum-cell {
height: 100%; padding: 2px;
display: flex;
align-items: center;
padding: 0 2px;
&--header { &--header {
border: 1px solid; border: 1px solid;
border-color: #8F8F8F #8F8F8F #171511 #171511; border-color: #8F8F8F #8F8F8F #171511 #171511;
white-space: nowrap;
padding-right: 5px;
padding-left: 5px;
} }
&--body { &--body {
border: 1px solid; border: 1px solid;
border-color: #000000 #000000 #252525 #252525; border-color: #000000 #000000 #161616 #161616;
position: relative;
} }
&--center { &--center {
justify-content: center; text-align: center;
} }
&--header-footer { &--header-footer {
justify-content: space-between; justify-content: space-between;
padding-right: 10px; padding-right: 10px;
} }
&__mod-controls {
display: none;
position: absolute;
top: -10px;
right: 5px;
background: #161616;
padding: 5px;
border: 1px solid $grey1;
a + a {
padding-left: 10px;
}
}
&:hover {
.forum-cell__mod-controls {
display: block;
}
}
} }
.thread__title { .thread__title {

View File

@@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { Link, RouteComponentProps } from 'react-router-dom'; import { Link, RouteComponentProps } from 'react-router-dom';
import { inject, observer } from 'mobx-react'; import { inject, observer } from 'mobx-react';
import { cloneDeep, filter, orderBy } from 'lodash'; import { cloneDeep, filter, orderBy, reject } from 'lodash';
import { ThreadService } from '../../services'; import { ThreadService, ModUpdate } from '../../services';
import { Editor, ForumNav, LoginButton, PaginationLinks, ScrollToTop } from '../../components'; import { Editor, ForumNav, LoginButton, PaginationLinks, ScrollToTop } from '../../components';
import { ThreadModel } from '../../model'; import { ThreadModel } from '../../model';
import { UserStore } from '../../stores/user-store'; import { UserStore } from '../../stores/user-store';
@@ -84,7 +84,9 @@ export class Forum extends React.Component<Props, State> {
// fetch threads from server // fetch threads from server
private async getThreads(categoryId: number) { private async getThreads(categoryId: number) {
const threads = await ThreadService.getCategoryThreads(categoryId); let threads = await ThreadService.getCategoryThreads(categoryId);
// remove hidden threads from normal users
threads = reject(threads, t => !this.props.userStore.isModOrAdmin() && t.hidden);
this.setState({ initialThreads: threads }); this.setState({ initialThreads: threads });
this.processThreads(threads); this.processThreads(threads);
} }
@@ -133,6 +135,11 @@ export class Forum extends React.Component<Props, State> {
this.processThreads(threads); this.processThreads(threads);
} }
private async onModItemClick(params: ModUpdate) {
await ThreadService.modUpdateThread(params);
this.getThreads(this.routeParams().categoryId);
}
private onNewTopic() { private onNewTopic() {
if (this.props.userStore.user) { if (this.props.userStore.user) {
this.setState({ showEditor: true }); this.setState({ showEditor: true });
@@ -161,7 +168,7 @@ export class Forum extends React.Component<Props, State> {
renderBody() { renderBody() {
return ( return (
<div className="forum-body"> <div>
<form className="flex" style={{ marginBottom: 0 }} onSubmit={e => this.onSearch(e)}> <form className="flex" style={{ marginBottom: 0 }} onSubmit={e => this.onSearch(e)}>
<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')}
@@ -183,35 +190,41 @@ export class Forum extends React.Component<Props, State> {
); );
} }
renderCell(content: JSX.Element | string, style: any, center?: boolean) {
let classNames: string = '';
classNames += center ? ' forum-cell--center' : '';
return <div className={`forum-cell flex-1 forum-cell--body ${classNames}`} style={style}>{content}</div>;
}
renderThreadRows() { renderThreadRows() {
const { categoryId } = this.routeParams(); const { categoryId } = this.routeParams();
return this.state.pageThreads.map((thread, index) => { return this.state.pageThreads.map((thread, index) => {
const authorBluePost = thread.user.permissions === 'admin' ? 'blue' : ''; const { id, sticky, hidden, last_reply, locked, reply_count, title, user, view_count } = thread;
const lastReplyBluePost = thread.last_reply.permissions === 'admin' ? 'blue' : ''; const authorBluePost = user.permissions === 'admin' ? 'blue' : '';
const sticky = thread.sticky ? <img src={stickyImage} title="Sticky"/> : ''; const lastReplyBluePost = last_reply.permissions === 'admin' ? 'blue' : '';
const stickyElement = sticky ? <img src={stickyImage} title="Sticky"/> : '';
return ( return (
<div className={`forum-row ${index % 2 === 0 && 'forum-row--dark'}`} key={index}> <tr className="forum-row forum-row__body" key={index}>
{this.renderCell(sticky, { maxWidth: '50px' }, true)} <td className={`forum-cell forum-cell--body forum-cell--center`}>{stickyElement}</td>
{this.renderCell( <td className={`forum-cell forum-cell--body`}>
<Link to={`/t/${categoryId}/${thread.id}`} className="thread__title">{thread.title}</Link>, <Link to={`/t/${categoryId}/${id}`} className="thread__title">{title}</Link>
{ minWidth: '200px' }, {this.props.userStore.isModOrAdmin() &&
)} <span className="forum-cell__mod-controls">
{this.renderCell(<b className={authorBluePost}>{thread.user.character_name || thread.user.battletag}</b>, { maxWidth: '150px' })} <a onClick={() => this.onModItemClick({ id, sticky: !sticky })}>{sticky ? 'Unstick' : 'Stick'}</a>
{this.renderCell(<b>{thread.reply_count}</b>, { maxWidth: '150px' }, true)} <a onClick={() => this.onModItemClick({ id, locked: !locked })}>{locked ? 'Unlock' : 'Lock'}</a>
{this.renderCell(<b>{thread.view_count}</b>, { maxWidth: '150px' }, true)} <a onClick={() => this.onModItemClick({ id, hidden: !hidden })}>{hidden ? 'Unhide' : 'Hide'}</a>
{this.renderCell( </span>
}
</td>
<td className={`forum-cell forum-cell--body`}>
<b className={authorBluePost}>{user.character_name || user.battletag}</b>
</td>
<td className={`forum-cell forum-cell--body forum-cell--center`}>
<b>{reply_count}</b>
</td>
<td className={`forum-cell forum-cell--body forum-cell--center`}>
<b>{view_count}</b>
</td>
<td className={`forum-cell forum-cell--body`}>
<div style={{ fontSize: '8pt' }}> <div style={{ fontSize: '8pt' }}>
by <b className={lastReplyBluePost}>{thread.last_reply.character_name || thread.last_reply.battletag}</b> by <b className={lastReplyBluePost}>{last_reply.character_name || last_reply.battletag}</b>
</div>, </div>
{ maxWidth: '200px' }, </td>
)} </tr>
</div>
); );
}); });
} }
@@ -233,22 +246,24 @@ export class Forum extends React.Component<Props, State> {
renderHeaderFooter() { renderHeaderFooter() {
const { categoryId, sortBy, threadsPerPage, sortOrder } = this.routeParams(); const { categoryId, sortBy, threadsPerPage, sortOrder } = this.routeParams();
return ( return (
<div className="forum-row forum-row--header"> <tr className="forum-table__header">
<div className="forum-cell forum-cell--header forum-cell--header-footer flex-1"> <td colSpan={100} className="forum-cell forum-cell--header">
<div className="flex"> <div className="flex forum-cell--header-footer">
<span style={{ marginRight: '10px' }}>Page:</span> <div className="flex">
<PaginationLinks <span style={{ marginRight: '10px' }}>Page:</span>
activePage={this.routeParams().page} <PaginationLinks
pageLinks={this.state.pageLinks} activePage={this.routeParams().page}
onPageSelect={page => this.navigateHere(categoryId, page, threadsPerPage, sortBy, sortOrder)} pageLinks={this.state.pageLinks}
/> onPageSelect={page => this.navigateHere(categoryId, page, threadsPerPage, sortBy, sortOrder)}
/>
</div>
<div>
<b>Threads/Page:</b>
{this.renderThreadsPerPageDropdown()}
</div>
</div> </div>
<div> </td>
<b>Threads/Page:</b> </tr>
{this.renderThreadsPerPageDropdown()}
</div>
</div>
</div>
); );
} }
@@ -257,49 +272,49 @@ export class Forum extends React.Component<Props, State> {
return show ? <img src={imgSrc}/> : null; return show ? <img src={imgSrc}/> : null;
} }
renderHeaderCell(columnHeader: ColumnHeader, style: any, center: boolean) { renderHeaderCell(columnHeader: ColumnHeader, center: boolean) {
const { categoryId, page, threadsPerPage, sortBy, sortOrder } = this.routeParams(); const { categoryId, page, threadsPerPage, sortBy, sortOrder } = this.routeParams();
const newSortOrder = sortOrder === 'asc' ? 'desc' : 'asc'; const newSortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
const centerClass = center ? 'forum-cell--center' : ''; const centerClass = center ? 'forum-cell--center' : '';
return ( return (
<div className={`forum-cell forum-cell--header flex-1 ${centerClass}`} style={style}> <td className={`forum-cell forum-cell--header ${centerClass}`}>
<a onClick={() => this.navigateHere(categoryId, page, threadsPerPage, columnHeader, newSortOrder)}> <a onClick={() => this.navigateHere(categoryId, page, threadsPerPage, columnHeader, newSortOrder)}>
<span>{columnHeader}</span> <span>{columnHeader}</span>
{this.renderSortingArrow(sortBy === columnHeader, sortOrder)} {this.renderSortingArrow(sortBy === columnHeader, sortOrder)}
</a> </a>
</div> </td>
); );
} }
renderTable() { renderTable() {
return ( return (
<div style={{ padding: '0 3px' }}> <div style={{ padding: '0 3px' }}>
<div className="forum-table"> <table className="forum-table">
<tbody>
{/* header */} {/* header */}
{this.renderHeaderFooter()} {this.renderHeaderFooter()}
{/* column headers */} <tr className="forum-table__header">
<div className="forum-row forum-row--header"> <td className={`forum-cell forum-cell--header forum-cell--center`} style={{ maxWidth: '50px' }}>
<div className={`forum-cell forum-cell--header flex-1 forum-cell--center`} style={{ maxWidth: '50px' }}> <img src={require('../../assets/flag.gif')}/>
<img src={require('../../assets/flag.gif')}/> </td>
</div> {this.renderHeaderCell(ColumnHeader.subject, false)}
{this.renderHeaderCell(ColumnHeader.subject, { minWidth: '200px' }, false)} {this.renderHeaderCell(ColumnHeader.author, true)}
{this.renderHeaderCell(ColumnHeader.author, { maxWidth: '150px' }, true)} {this.renderHeaderCell(ColumnHeader.replies, true)}
{this.renderHeaderCell(ColumnHeader.replies, { maxWidth: '150px' }, true)} {this.renderHeaderCell(ColumnHeader.views, true)}
{this.renderHeaderCell(ColumnHeader.views, { maxWidth: '150px' }, true)} {this.renderHeaderCell(ColumnHeader.lastPost, true)}
{this.renderHeaderCell(ColumnHeader.lastPost, { maxWidth: '200px' }, true)} </tr>
</div>
{/* table body */} {/* body */}
{this.renderThreadRows()} {this.renderThreadRows()}
{/* footer */} {/* footer */}
{this.renderHeaderFooter()} {this.renderHeaderFooter()}
</div>
</tbody>
</table>
<div className="forumliner-bot-bg"/> <div className="forumliner-bot-bg"/>
</div> </div>
); );

View File

@@ -70,7 +70,9 @@ export class Thread extends React.Component<Props, State> {
private processReplies(thread: ThreadModel, props: Props = this.props) { private processReplies(thread: ThreadModel, props: Props = this.props) {
thread.replies = chain(thread.replies) thread.replies = chain(thread.replies)
.orderBy(['inserted_at'], ['asc']) .orderBy(['inserted_at'], ['asc'])
.map((t, i) => { t.index = i; return t; }) .map((r, i) => { r.index = i; return r; })
// remove hidden replies only for normal users
.reject(r => !this.props.userStore.isModOrAdmin() && r.hidden)
.value(); .value();
const { page } = this.routeParams(props); const { page } = this.routeParams(props);
const numPages = Math.ceil(thread.replies.length / 20); const numPages = Math.ceil(thread.replies.length / 20);
@@ -114,6 +116,11 @@ export class Thread extends React.Component<Props, State> {
} }
} }
private async onModButtonClick(params: { id: number, hidden?: boolean}) {
await ThreadService.modUpdateReply(params);
this.getReplies();
}
private navigateForumIndex() { private navigateForumIndex() {
this.props.history.push(`/f/${this.state.thread!.category_id}`); this.props.history.push(`/f/${this.state.thread!.category_id}`);
} }
@@ -161,6 +168,17 @@ export class Thread extends React.Component<Props, State> {
} }
} }
renderModButtons(reply: ReplyModel, index: number): any {
const { id, hidden } = reply;
if (index !== 0 && this.props.userStore.isModOrAdmin()) {
return (
<a style={{ paddingRight: '10px' }} onClick={() => this.onModButtonClick({ id, hidden: !hidden })}>
{hidden ? <span className="red">Unhide</span> : <span>Hide</span>}
</a>
);
}
}
renderReplies(): any { renderReplies(): any {
return this.state.replies.map((reply, index) => { return this.state.replies.map((reply, index) => {
const replyDark = index % 2 === 0 ? 'reply--dark' : ''; const replyDark = index % 2 === 0 ? 'reply--dark' : '';
@@ -177,6 +195,7 @@ export class Thread extends React.Component<Props, State> {
<small style={{ paddingLeft: '5px' }}>| {this.getTimeFormat(reply.inserted_at)}</small> <small style={{ paddingLeft: '5px' }}>| {this.getTimeFormat(reply.inserted_at)}</small>
</div> </div>
<div className="flex flex--center"> <div className="flex flex--center">
{this.renderModButtons(reply, index)}
{this.renderEditbutton(reply)} {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"

View File

@@ -23,7 +23,34 @@ const getThread = async (thread_id: string | number): Promise<ThreadModel> => {
return [] as any; return [] as any;
}; };
export interface ModUpdate {
id: number;
sticky?: boolean;
locked?: boolean;
hidden?: boolean;
}
const modUpdateThread = async (params: ModUpdate): Promise<any> => {
try {
const res = await axios.put('/api/thread/mod', params);
return res.data.data;
} catch (e) {
console.log(e);
}
};
const modUpdateReply = async (params: { id: number, hidden?: boolean }): Promise<any> => {
try {
const res = await axios.put('/api/reply/mod', params);
return res.data.data;
} catch (e) {
console.log(e);
}
};
export const ThreadService = { export const ThreadService = {
getCategoryThreads, getCategoryThreads,
getThread, getThread,
modUpdateReply,
modUpdateThread,
}; };

View File

@@ -44,6 +44,9 @@ export class UserStore {
localStorage.removeItem('user'); localStorage.removeItem('user');
} }
@action isModOrAdmin() {
return this.user && this.user.permissions.match(/mod|admin/);
}
} }
export default new UserStore(); export default new UserStore();

View File

@@ -58,6 +58,7 @@ defmodule MyApp.Data.Thread do
:updated_at, :updated_at,
:inserted_at, :inserted_at,
:sticky, :sticky,
:hidden,
:locked, :locked,
:last_reply_id, :last_reply_id,
:category_id, :category_id,