From ce151bc0c0bcbfd5617d8ecbc876ed4220bd5a97 Mon Sep 17 00:00:00 2001 From: Mitchell Date: Thu, 25 Jan 2018 22:43:56 -0600 Subject: [PATCH] client - admin update thread/reply for locked/hidden/sticky --- client/app/model/reply.ts | 1 + client/app/model/thread.ts | 1 + client/app/pages/forum/forum.scss | 56 ++++++---- client/app/pages/forum/forum.tsx | 147 ++++++++++++++------------ client/app/pages/thread/thread.tsx | 21 +++- client/app/services/thread.service.ts | 27 +++++ client/app/stores/user-store.ts | 3 + lib/myapp/data/thread.ex | 1 + 8 files changed, 171 insertions(+), 86 deletions(-) diff --git a/client/app/model/reply.ts b/client/app/model/reply.ts index cc3cd85..a09f4f7 100644 --- a/client/app/model/reply.ts +++ b/client/app/model/reply.ts @@ -3,6 +3,7 @@ import { UserModel } from './user'; export interface ReplyModel { content: string; edited: boolean; + hidden: boolean; id: number; index?: number; inserted_at: string; diff --git a/client/app/model/thread.ts b/client/app/model/thread.ts index 31fe95a..2686420 100644 --- a/client/app/model/thread.ts +++ b/client/app/model/thread.ts @@ -5,6 +5,7 @@ export interface ThreadModel { category_id: number; title: string; edited: boolean; + hidden: boolean; id: number; inserted_at: string; last_reply: UserModel; diff --git a/client/app/pages/forum/forum.scss b/client/app/pages/forum/forum.scss index 18da467..a0f69dd 100644 --- a/client/app/pages/forum/forum.scss +++ b/client/app/pages/forum/forum.scss @@ -8,11 +8,6 @@ $grey2: #161616; justify-content: space-between; } -.forum-body { - min-height: 500px; - margin-bottom: 100px; -} - .forum-menu-search-bg { background-image: url('../../assets/forum-menu-search-bg.gif'); @@ -36,6 +31,7 @@ $grey2: #161616; .forum-table { width: 100%; + border-spacing: 0; border: 1px solid; border-color: #575757; 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 { - display: flex; - align-items: center; - height: 24px; background: $grey1; - &--header { - background-image: url('../../assets/thread-topic-bg2.gif'); - background-repeat: repeat-x; + &__body { + height: 24px; } - &--dark { + &:nth-child(even) { background: $grey2; } } .forum-cell { - height: 100%; - display: flex; - align-items: center; - padding: 0 2px; + padding: 2px; &--header { border: 1px solid; border-color: #8F8F8F #8F8F8F #171511 #171511; + white-space: nowrap; + padding-right: 5px; + padding-left: 5px; } &--body { border: 1px solid; - border-color: #000000 #000000 #252525 #252525; + border-color: #000000 #000000 #161616 #161616; + position: relative; } &--center { - justify-content: center; + text-align: center; } - &--header-footer { justify-content: space-between; 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 { diff --git a/client/app/pages/forum/forum.tsx b/client/app/pages/forum/forum.tsx index 7e2a499..c571a65 100644 --- a/client/app/pages/forum/forum.tsx +++ b/client/app/pages/forum/forum.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { Link, RouteComponentProps } from 'react-router-dom'; import { inject, observer } from 'mobx-react'; -import { cloneDeep, filter, orderBy } from 'lodash'; -import { ThreadService } from '../../services'; +import { cloneDeep, filter, orderBy, reject } from 'lodash'; +import { ThreadService, ModUpdate } from '../../services'; import { Editor, ForumNav, LoginButton, PaginationLinks, ScrollToTop } from '../../components'; import { ThreadModel } from '../../model'; import { UserStore } from '../../stores/user-store'; @@ -84,7 +84,9 @@ export class Forum extends React.Component { // fetch threads from server 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.processThreads(threads); } @@ -133,6 +135,11 @@ export class Forum extends React.Component { this.processThreads(threads); } + private async onModItemClick(params: ModUpdate) { + await ThreadService.modUpdateThread(params); + this.getThreads(this.routeParams().categoryId); + } + private onNewTopic() { if (this.props.userStore.user) { this.setState({ showEditor: true }); @@ -161,7 +168,7 @@ export class Forum extends React.Component { renderBody() { return ( -
+
this.onSearch(e)}> { ); } - renderCell(content: JSX.Element | string, style: any, center?: boolean) { - let classNames: string = ''; - classNames += center ? ' forum-cell--center' : ''; - return
{content}
; - } - renderThreadRows() { const { categoryId } = this.routeParams(); return this.state.pageThreads.map((thread, index) => { - const authorBluePost = thread.user.permissions === 'admin' ? 'blue' : ''; - const lastReplyBluePost = thread.last_reply.permissions === 'admin' ? 'blue' : ''; - const sticky = thread.sticky ? : ''; + const { id, sticky, hidden, last_reply, locked, reply_count, title, user, view_count } = thread; + const authorBluePost = user.permissions === 'admin' ? 'blue' : ''; + const lastReplyBluePost = last_reply.permissions === 'admin' ? 'blue' : ''; + const stickyElement = sticky ? : ''; return ( -
- {this.renderCell(sticky, { maxWidth: '50px' }, true)} - {this.renderCell( - {thread.title}, - { minWidth: '200px' }, - )} - {this.renderCell({thread.user.character_name || thread.user.battletag}, { maxWidth: '150px' })} - {this.renderCell({thread.reply_count}, { maxWidth: '150px' }, true)} - {this.renderCell({thread.view_count}, { maxWidth: '150px' }, true)} - {this.renderCell( + + {stickyElement} + + {title} + {this.props.userStore.isModOrAdmin() && + + this.onModItemClick({ id, sticky: !sticky })}>{sticky ? 'Unstick' : 'Stick'} + this.onModItemClick({ id, locked: !locked })}>{locked ? 'Unlock' : 'Lock'} + this.onModItemClick({ id, hidden: !hidden })}>{hidden ? 'Unhide' : 'Hide'} + + } + + + {user.character_name || user.battletag} + + + {reply_count} + + + {view_count} + +
- by {thread.last_reply.character_name || thread.last_reply.battletag} -
, - { maxWidth: '200px' }, - )} -
+ by {last_reply.character_name || last_reply.battletag} +
+ + ); }); } @@ -233,22 +246,24 @@ export class Forum extends React.Component { renderHeaderFooter() { const { categoryId, sortBy, threadsPerPage, sortOrder } = this.routeParams(); return ( -
-
-
- Page: - this.navigateHere(categoryId, page, threadsPerPage, sortBy, sortOrder)} - /> + + +
+
+ Page: + this.navigateHere(categoryId, page, threadsPerPage, sortBy, sortOrder)} + /> +
+
+ Threads/Page: + {this.renderThreadsPerPageDropdown()} +
-
- Threads/Page: - {this.renderThreadsPerPageDropdown()} -
-
-
+ + ); } @@ -257,49 +272,49 @@ export class Forum extends React.Component { return show ? : null; } - renderHeaderCell(columnHeader: ColumnHeader, style: any, center: boolean) { + renderHeaderCell(columnHeader: ColumnHeader, center: boolean) { const { categoryId, page, threadsPerPage, sortBy, sortOrder } = this.routeParams(); const newSortOrder = sortOrder === 'asc' ? 'desc' : 'asc'; const centerClass = center ? 'forum-cell--center' : ''; return ( - + ); } renderTable() { return (
-
+ + - {/* header */} - {this.renderHeaderFooter()} + {/* header */} + {this.renderHeaderFooter()} - {/* column headers */} -
-
- -
- {this.renderHeaderCell(ColumnHeader.subject, { minWidth: '200px' }, false)} - {this.renderHeaderCell(ColumnHeader.author, { maxWidth: '150px' }, true)} - {this.renderHeaderCell(ColumnHeader.replies, { maxWidth: '150px' }, true)} - {this.renderHeaderCell(ColumnHeader.views, { maxWidth: '150px' }, true)} - {this.renderHeaderCell(ColumnHeader.lastPost, { maxWidth: '200px' }, true)} -
+ + + {this.renderHeaderCell(ColumnHeader.subject, false)} + {this.renderHeaderCell(ColumnHeader.author, true)} + {this.renderHeaderCell(ColumnHeader.replies, true)} + {this.renderHeaderCell(ColumnHeader.views, true)} + {this.renderHeaderCell(ColumnHeader.lastPost, true)} + - {/* table body */} - {this.renderThreadRows()} + {/* body */} + {this.renderThreadRows()} - {/* footer */} - {this.renderHeaderFooter()} - - + {/* footer */} + {this.renderHeaderFooter()} + +
+ +
); diff --git a/client/app/pages/thread/thread.tsx b/client/app/pages/thread/thread.tsx index 9849c61..b4c3733 100644 --- a/client/app/pages/thread/thread.tsx +++ b/client/app/pages/thread/thread.tsx @@ -70,7 +70,9 @@ export class Thread extends React.Component { private processReplies(thread: ThreadModel, props: Props = this.props) { thread.replies = chain(thread.replies) .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(); const { page } = this.routeParams(props); const numPages = Math.ceil(thread.replies.length / 20); @@ -114,6 +116,11 @@ export class Thread extends React.Component { } } + private async onModButtonClick(params: { id: number, hidden?: boolean}) { + await ThreadService.modUpdateReply(params); + this.getReplies(); + } + private navigateForumIndex() { this.props.history.push(`/f/${this.state.thread!.category_id}`); } @@ -161,6 +168,17 @@ export class Thread extends React.Component { } } + renderModButtons(reply: ReplyModel, index: number): any { + const { id, hidden } = reply; + if (index !== 0 && this.props.userStore.isModOrAdmin()) { + return ( + this.onModButtonClick({ id, hidden: !hidden })}> + {hidden ? Unhide : Hide} + + ); + } + } + renderReplies(): any { return this.state.replies.map((reply, index) => { const replyDark = index % 2 === 0 ? 'reply--dark' : ''; @@ -177,6 +195,7 @@ export class Thread extends React.Component { | {this.getTimeFormat(reply.inserted_at)}
+ {this.renderModButtons(reply, index)} {this.renderEditbutton(reply)} => { return [] as any; }; +export interface ModUpdate { + id: number; + sticky?: boolean; + locked?: boolean; + hidden?: boolean; +} + +const modUpdateThread = async (params: ModUpdate): Promise => { + 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 => { + try { + const res = await axios.put('/api/reply/mod', params); + return res.data.data; + } catch (e) { + console.log(e); + } +}; + export const ThreadService = { getCategoryThreads, getThread, + modUpdateReply, + modUpdateThread, }; diff --git a/client/app/stores/user-store.ts b/client/app/stores/user-store.ts index 58b7181..2f1985e 100644 --- a/client/app/stores/user-store.ts +++ b/client/app/stores/user-store.ts @@ -44,6 +44,9 @@ export class UserStore { localStorage.removeItem('user'); } + @action isModOrAdmin() { + return this.user && this.user.permissions.match(/mod|admin/); + } } export default new UserStore(); diff --git a/lib/myapp/data/thread.ex b/lib/myapp/data/thread.ex index e2cb302..c4ed3ad 100644 --- a/lib/myapp/data/thread.ex +++ b/lib/myapp/data/thread.ex @@ -58,6 +58,7 @@ defmodule MyApp.Data.Thread do :updated_at, :inserted_at, :sticky, + :hidden, :locked, :last_reply_id, :category_id,