Mod Center MVP 🎉 (#8744)
Co-authored-by: Andy Zhao <17884966+Zhao-Andy@users.noreply.github.com>
This commit is contained in:
parent
40f54e317f
commit
e45742c212
13 changed files with 492 additions and 227 deletions
|
|
@ -2,163 +2,87 @@
|
|||
@import 'mixins';
|
||||
@import 'config/import';
|
||||
|
||||
.mod-index-hero {
|
||||
margin: 0 auto;
|
||||
padding: var(--su-7) var(--su-1);
|
||||
text-align: center;
|
||||
background: var(--body-bg);
|
||||
h1 {
|
||||
font-size: calc(1.8em + 2vw);
|
||||
}
|
||||
h2 {
|
||||
font-size: calc(0.9em + 1vw);
|
||||
}
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.mod-index-hero-data-pod {
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
width: 90%;
|
||||
font-weight: bold;
|
||||
border: 3px solid var(--body-color);
|
||||
padding: 20px 0px;
|
||||
.mod-index-hero-data-pod-number {
|
||||
font-size: 2.8em;
|
||||
}
|
||||
@media screen and (min-width: 960px) {
|
||||
width: calc(190px + 11vw);
|
||||
padding: 80px 0px;
|
||||
margin: 15px 2px;
|
||||
.mod-index-hero-data-pod-number {
|
||||
font-size: calc(2.7em + 0.2vw);
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1300px) {
|
||||
width: calc(260px + 8vw);
|
||||
padding: 80px 0px;
|
||||
margin: 30px;
|
||||
.mod-index-hero-data-pod-number {
|
||||
font-size: calc(2.8em + 0.5vw);
|
||||
}
|
||||
}
|
||||
}
|
||||
.mod-index-hero-sub {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.mod-center-heading {
|
||||
margin-bottom: var(--su-6);
|
||||
}
|
||||
|
||||
.mod-index-header {
|
||||
margin: 50px auto 30px;
|
||||
width: 96%;
|
||||
max-width: 1600px;
|
||||
padding-left: 20px;
|
||||
.mod-index-header-top-filters {
|
||||
padding: 10px 0px 25px;
|
||||
a {
|
||||
color: var(--body-color);
|
||||
font-size: 0.86em;
|
||||
padding: 3px 18px;
|
||||
border-radius: 100px;
|
||||
border: 1px solid transparent;
|
||||
&.mod-index-top-filter-link--active {
|
||||
border: 1px solid var(--body-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
a.mod-index-header-link {
|
||||
color: $black;
|
||||
padding: 5px 15px;
|
||||
border-radius: 3px;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
a.mod-index-header-link--active {
|
||||
@include themeable(border, theme-border, 1px solid $black);
|
||||
}
|
||||
}
|
||||
.mod-index-container {
|
||||
.mod-index-header {
|
||||
background-color: var(--base-inverted);
|
||||
border-radius: var(--radius) var(--radius) 0 0;
|
||||
border-bottom: 1px solid var(--base-10);
|
||||
padding: var(--su-6) var(--su-4);
|
||||
|
||||
.mod-index-list {
|
||||
margin: 5px auto;
|
||||
width: 96%;
|
||||
max-width: 1600px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--card-color-tertiary);
|
||||
background: var(--card-bg);
|
||||
|
||||
.mod-index-row {
|
||||
padding: 10px;
|
||||
@include themeable(
|
||||
border-bottom,
|
||||
theme-border,
|
||||
1px solid $light-medium-gray
|
||||
);
|
||||
a.mod-index-article-title {
|
||||
font-size: calc(1.2em + 0.3vw);
|
||||
font-weight: bold;
|
||||
margin-right: 6px;
|
||||
display: inline-block;
|
||||
color: var(--body-color);
|
||||
.tag-view {
|
||||
color: var(--base-90);
|
||||
}
|
||||
.mod-index-article-tagslist {
|
||||
display: inline-block;
|
||||
}
|
||||
.mod-index-list-header {
|
||||
display: grid;
|
||||
grid-template-columns: 4fr 2fr 1fr;
|
||||
background-color: var(--base-0);
|
||||
border: solid var(--base-10);
|
||||
border-width: 1px 0;
|
||||
h2 {
|
||||
padding: var(--su-2) var(--su-4);
|
||||
color: var(--base-50);
|
||||
}
|
||||
a.mod-index-article-tag {
|
||||
font-size: 0.9em;
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
background: $light-gray;
|
||||
color: $black;
|
||||
vertical-align: 0.2em;
|
||||
}
|
||||
.mod-index-actions-and-details {
|
||||
img {
|
||||
height: 1.4em;
|
||||
width: 1.4em;
|
||||
border-radius: 100px;
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
vertical-align: -0.3em;
|
||||
}
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: var(--card-color-tertiary);
|
||||
}
|
||||
}
|
||||
.quick-mod-button {
|
||||
font-size: 0.95em;
|
||||
padding: 5px 18px;
|
||||
border-radius: 100px;
|
||||
@include themeable(border, theme-border, 1px solid $medium-gray);
|
||||
display: inline-block;
|
||||
margin: 10px 0px 5px;
|
||||
margin-right: 5px;
|
||||
background: transparent;
|
||||
color: var(--body-color);
|
||||
}
|
||||
.mod-index-row-iframes {
|
||||
padding: 5px 0px;
|
||||
display: flex;
|
||||
iframe {
|
||||
height: calc(100vh - 100px);
|
||||
max-height: 800px;
|
||||
}
|
||||
.mod-index-list {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
.moderation-articles-list {
|
||||
.moderation-single-article {
|
||||
width: 100%;
|
||||
border: 0px;
|
||||
display: inline-block;
|
||||
}
|
||||
.actions-panel {
|
||||
width: 36%;
|
||||
background-color: var(--base-inverted);
|
||||
text-align: left;
|
||||
padding: var(--su-4);
|
||||
border: solid var(--base-10);
|
||||
border-width: 1px 0;
|
||||
display: grid;
|
||||
grid-template-columns: 4fr 2fr 1fr;
|
||||
.mod-article-tag {
|
||||
color: var(--base-80);
|
||||
padding-right: var(--su-2);
|
||||
.article-hash-tag {
|
||||
color: var(--base-40);
|
||||
}
|
||||
}
|
||||
.article-author {
|
||||
color: var(--base-80);
|
||||
padding-left: var(--su-4);
|
||||
}
|
||||
.article-published-at {
|
||||
color: var(--base-90);
|
||||
padding-left: var(--su-6);
|
||||
}
|
||||
.article-iframes-container {
|
||||
border-radius: var(--radius);
|
||||
margin-top: var(--su-3);
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 4;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
&.opened {
|
||||
border: 1px solid var(--base-20);
|
||||
}
|
||||
iframe {
|
||||
border: none;
|
||||
}
|
||||
.article-iframe {
|
||||
width: 100%;
|
||||
height: 650px; // height is arbitrary, will be removed for modal-esque view
|
||||
}
|
||||
.actions-panel-iframe {
|
||||
width: 60%;
|
||||
height: 650px; // height is arbitrary, will be removed for modal-esque view
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mod-index-footer {
|
||||
text-align: center;
|
||||
padding: 30px 10px;
|
||||
}
|
||||
|
||||
// these styles are to correct height and margin issues in the iframe
|
||||
.moderations-actions_panel {
|
||||
min-height: initial;
|
||||
|
|
@ -763,3 +687,29 @@
|
|||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mod-center-section-title {
|
||||
padding: var(--su-2);
|
||||
|
||||
&.resources {
|
||||
margin-top: var(--su-7);
|
||||
}
|
||||
}
|
||||
|
||||
.mod-center-inbox {
|
||||
.inbox-tags {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-left: var(--su-3);
|
||||
padding-right: var(--su-3);
|
||||
}
|
||||
}
|
||||
|
||||
#mod-center-other-options svg {
|
||||
width: var(--su-3);
|
||||
}
|
||||
|
||||
.mod-feedback {
|
||||
margin-top: var(--su-6);
|
||||
margin-left: var(--su-3);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,26 @@
|
|||
class ModerationsController < ApplicationController
|
||||
after_action :verify_authorized
|
||||
|
||||
JSON_OPTIONS = {
|
||||
only: %i[id title published_at cached_tag_list path],
|
||||
include: {
|
||||
user: { only: %i[username name path articles_count] }
|
||||
}
|
||||
}.freeze
|
||||
|
||||
def index
|
||||
skip_authorization
|
||||
return unless current_user&.trusted
|
||||
|
||||
@articles = Article.published.
|
||||
articles = Article.published.
|
||||
where("score > -5 AND score < 5").
|
||||
order("published_at DESC").limit(70)
|
||||
@articles = @articles.cached_tagged_with(params[:tag]) if params[:tag].present?
|
||||
@articles = @articles.where("nth_published_by_author > 0 AND nth_published_by_author < 4 AND published_at > ?", 7.days.ago) if params[:state] == "new-authors"
|
||||
@articles = @articles.decorate
|
||||
articles = articles.cached_tagged_with(params[:tag]) if params[:tag].present?
|
||||
articles = articles.where("nth_published_by_author > 0 AND nth_published_by_author < 4 AND published_at > ?", 7.days.ago) if params[:state] == "new-authors"
|
||||
@articles = articles.includes(:user).to_json(JSON_OPTIONS)
|
||||
@tag = Tag.find_by(name: params[:tag]) || not_found if params[:tag].present?
|
||||
@current_user_tags = current_user.moderator_for_tags
|
||||
@community_mod_channel = current_user.chat_channels.find_by("channel_name LIKE ?", "Community Mods: Team%")
|
||||
end
|
||||
|
||||
def article
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ export default function initializeActionsPanel(user, path) {
|
|||
|
||||
document.querySelector('.mod-actions-menu').innerHTML = modActionsMenuHTML;
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
if (!top.document.location.pathname.endsWith('/mod')) {
|
||||
if (!top.document.location.pathname.includes('/mod')) {
|
||||
// don't show mod button in mod center page
|
||||
document.getElementById(
|
||||
'mod-actions-menu-btn-area',
|
||||
).innerHTML = modActionsMenuIconHTML;
|
||||
|
|
|
|||
40
app/javascript/modCenter/moderationArticles.jsx
Normal file
40
app/javascript/modCenter/moderationArticles.jsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { h, Component } from 'preact';
|
||||
import SingleArticle from './singleArticle';
|
||||
|
||||
export class ModerationArticles extends Component {
|
||||
state = {
|
||||
articles: JSON.parse(
|
||||
document.getElementById('mod-index-list').dataset.articles,
|
||||
),
|
||||
};
|
||||
|
||||
render() {
|
||||
const { articles } = this.state;
|
||||
|
||||
return (
|
||||
<div className="moderation-articles-list">
|
||||
{articles.map((article) => {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
path,
|
||||
cached_tag_list: cachedTagList,
|
||||
published_at: publishedAt,
|
||||
user,
|
||||
} = article;
|
||||
return (
|
||||
<SingleArticle
|
||||
id={id}
|
||||
title={title}
|
||||
path={path}
|
||||
cachedTagList={cachedTagList}
|
||||
key={id}
|
||||
publishedAt={publishedAt}
|
||||
user={user}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
13
app/javascript/modCenter/singleArticle/articlePropTypes.js
Normal file
13
app/javascript/modCenter/singleArticle/articlePropTypes.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
export default PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
path: PropTypes.string,
|
||||
cached_tag_list: PropTypes.arrayOf(PropTypes.string),
|
||||
user: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
}),
|
||||
});
|
||||
82
app/javascript/modCenter/singleArticle/index.jsx
Normal file
82
app/javascript/modCenter/singleArticle/index.jsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h, Component } from 'preact';
|
||||
import { formatDate } from './util';
|
||||
|
||||
export default class SingleArticle extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
articleOpened: false,
|
||||
};
|
||||
}
|
||||
|
||||
toggleArticle = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { id, path } = this.props;
|
||||
const { articleOpened } = this.state;
|
||||
if (articleOpened) {
|
||||
this.setState({ articleOpened: false });
|
||||
document.getElementById(`article-iframe-${id}`).innerHTML = '';
|
||||
} else {
|
||||
this.setState({ articleOpened: true });
|
||||
document.getElementById(
|
||||
`article-iframe-${id}`,
|
||||
).innerHTML = `<iframe class="article-iframe" src="${path}"></iframe><iframe class="actions-panel-iframe" src="${path}/actions_panel"></iframe>`;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { articleOpened } = this.state;
|
||||
const { id, title, publishedAt, cachedTagList, user, key } = this.props;
|
||||
const tags = cachedTagList.split(', ').map((tag) => {
|
||||
return (
|
||||
<span className="mod-article-tag fs-s ff-accent lh-base" key={key}>
|
||||
<span className="article-hash-tag">#</span>
|
||||
{tag}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
const newAuthorNotification = user.articles_count <= 3 ? '👋 ' : '';
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="moderation-single-article"
|
||||
onClick={this.toggleArticle}
|
||||
>
|
||||
<span className="article-title">
|
||||
<header>
|
||||
<h3 className="fs-base fw-bold lh-tight">{title}</h3>
|
||||
</header>
|
||||
{tags}
|
||||
</span>
|
||||
<span className="article-author fs-s lw-medium lh-tight">
|
||||
{newAuthorNotification}
|
||||
{user.name}
|
||||
</span>
|
||||
<span className="article-published-at fs-s fw-bold lh-tight">
|
||||
<time dateTime={publishedAt}>{formatDate(publishedAt)}</time>
|
||||
</span>
|
||||
<div
|
||||
className={`article-iframes-container ${
|
||||
articleOpened ? 'opened' : ''
|
||||
}`}
|
||||
id={`article-iframe-${id}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SingleArticle.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
publishedAt: PropTypes.string.isRequired,
|
||||
cachedTagList: PropTypes.isRequired,
|
||||
user: PropTypes.isRequired,
|
||||
key: PropTypes.number.isRequired,
|
||||
};
|
||||
103
app/javascript/modCenter/singleArticle/singleArticle.test.jsx
Normal file
103
app/javascript/modCenter/singleArticle/singleArticle.test.jsx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { h } from 'preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { render, getNodeText, fireEvent } from '@testing-library/preact';
|
||||
|
||||
import SingleArticle from './index';
|
||||
|
||||
const testArticle = {
|
||||
id: 1,
|
||||
title: 'An article title',
|
||||
path: 'an-article-title-di3',
|
||||
publishedAt: '2019-06-22T16:11:10.590Z',
|
||||
cachedTagList: 'discuss, javascript, beginners',
|
||||
user: {
|
||||
articles_count: 1,
|
||||
name: 'hello',
|
||||
},
|
||||
};
|
||||
|
||||
describe('<SingleArticle />', () => {
|
||||
const renderSingleArticle = (article = testArticle) =>
|
||||
render(
|
||||
<SingleArticle
|
||||
id={article.title}
|
||||
title={article.title}
|
||||
path={article.path}
|
||||
publishedAt={article.publishedAt} // renders as Jun 28
|
||||
cachedTagList={article.cachedTagList}
|
||||
user={article.user}
|
||||
/>,
|
||||
);
|
||||
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = renderSingleArticle();
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('renders the article title', () => {
|
||||
const { getByText } = renderSingleArticle();
|
||||
getByText(testArticle.title);
|
||||
});
|
||||
|
||||
it('renders the tags', () => {
|
||||
const { getByText } = renderSingleArticle();
|
||||
getByText('discuss');
|
||||
getByText('javascript');
|
||||
getByText('beginners');
|
||||
});
|
||||
it('renders the author name', () => {
|
||||
const { container } = renderSingleArticle();
|
||||
const text = getNodeText(container.querySelector('.article-author'));
|
||||
expect(text).toContain(testArticle.user.name);
|
||||
});
|
||||
it('renders the hand wave emoji if the author has less than 3 articles ', () => {
|
||||
const { container } = renderSingleArticle();
|
||||
const text = getNodeText(container.querySelector('.article-author'));
|
||||
expect(text).toContain('👋');
|
||||
});
|
||||
it('renders the correct formatted published date', () => {
|
||||
const { getByText } = renderSingleArticle();
|
||||
getByText('Jun 22');
|
||||
});
|
||||
it('renders the correct formatted published date as a time if the date is the same day', () => {
|
||||
const today = new Date();
|
||||
today.setSeconds('00');
|
||||
testArticle.publishedAt = today.toISOString();
|
||||
const { getByText } = renderSingleArticle(testArticle);
|
||||
const readableTime = today.toLocaleTimeString().replace(':00 ', ' '); // looks like 8:05 PM
|
||||
getByText(readableTime);
|
||||
});
|
||||
it('renders the iframes on click', () => {
|
||||
const { container } = renderSingleArticle();
|
||||
container.querySelector('button.moderation-single-article').click();
|
||||
const iframes = container.querySelectorAll('iframe');
|
||||
expect(iframes.length).toEqual(2);
|
||||
|
||||
const [articleIframe, actionPanelIframe] = iframes;
|
||||
expect(articleIframe.src).toContain(testArticle.path);
|
||||
expect(actionPanelIframe.src).toContain(
|
||||
`${testArticle.path}/actions_panel`,
|
||||
);
|
||||
});
|
||||
it('adds the opened class when opening an article', () => {
|
||||
const toggleArticle = jest.fn();
|
||||
const { container } = render(
|
||||
<SingleArticle
|
||||
id={testArticle.title}
|
||||
title={testArticle.title}
|
||||
path={testArticle.path}
|
||||
publishedAt={testArticle.publishedAt} // renders as Jun 28
|
||||
cachedTagList={testArticle.cachedTagList}
|
||||
user={testArticle.user}
|
||||
toggleArticle={toggleArticle}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(
|
||||
container.querySelector('button.moderation-single-article'),
|
||||
);
|
||||
expect(
|
||||
container.querySelector('.article-iframes-container').classList,
|
||||
).toContain('opened');
|
||||
});
|
||||
});
|
||||
20
app/javascript/modCenter/singleArticle/util.js
Normal file
20
app/javascript/modCenter/singleArticle/util.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
const time_options = {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
};
|
||||
|
||||
const date_options = {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
};
|
||||
|
||||
export const formatDate = (timestamp, currentLocale) => {
|
||||
const dateToday = new Date();
|
||||
const articlePublished = new Date(timestamp);
|
||||
const locale = currentLocale || 'default';
|
||||
|
||||
if (dateToday.toDateString() === articlePublished.toDateString()) {
|
||||
return articlePublished.toLocaleString(locale, time_options);
|
||||
}
|
||||
return articlePublished.toLocaleString(locale, date_options);
|
||||
};
|
||||
38
app/javascript/packs/modCenter.jsx
Normal file
38
app/javascript/packs/modCenter.jsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { h, render } from 'preact';
|
||||
import { ModerationArticles } from '../modCenter/moderationArticles';
|
||||
import { addSnackbarItem, Snackbar } from '../Snackbar';
|
||||
|
||||
let elementLoaded = false;
|
||||
|
||||
function loadElement() {
|
||||
const root = document.getElementById('mod-index-list');
|
||||
const isMobileDevice = typeof window.orientation !== 'undefined';
|
||||
const snackZone = document.getElementById('snack-zone');
|
||||
|
||||
if (root) {
|
||||
render(<ModerationArticles />, root);
|
||||
}
|
||||
|
||||
if (snackZone) {
|
||||
render(<Snackbar lifespan="0" />, snackZone);
|
||||
}
|
||||
|
||||
if (isMobileDevice) {
|
||||
addSnackbarItem({
|
||||
message: 'The Mod Center is best viewed on desktop.',
|
||||
addCloseButton: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.InstantClick.on('change', () => {
|
||||
if (!elementLoaded) {
|
||||
loadElement();
|
||||
elementLoaded = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!elementLoaded) {
|
||||
loadElement();
|
||||
elementLoaded = true;
|
||||
}
|
||||
51
app/views/moderations/_mod_sidebar_left.html.erb
Normal file
51
app/views/moderations/_mod_sidebar_left.html.erb
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<div class="crayons-layout__sidebar-left">
|
||||
<p class="fw-heavy fs-3xl mod-center-heading">Mod Center</p>
|
||||
<div class="fw-bold fs-s mod-center-section-title">
|
||||
INBOX
|
||||
</div>
|
||||
<div id="mod-center-inbox" class="mod-center-inbox">
|
||||
<% if @current_user_tags.any? %>
|
||||
<% @current_user_tags.each do |tag| %>
|
||||
<a href="<%= "#{URL.url}/mod/#{tag}" %>" class="crayons-link crayons-link--block inbox-tags<%= " crayons-link--current" if tag == @tag&.name %>" data-tag-name="<%= tag %>">
|
||||
#<%= tag %>
|
||||
<span id="<%= tag %>-indicator" class="crayons-indicator"></span>
|
||||
</a>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<a
|
||||
href="<%= "#{URL.url}/mod" %>"
|
||||
class="crayons-link crayons-link--block inbox-tags<%= " crayons-link--current" if @tag.blank? %>"
|
||||
data-tag-name="All topics">
|
||||
All topics
|
||||
</a>
|
||||
</div>
|
||||
<div class="fw-bold fs-s mod-center-section-title resources">RESOURCES</div>
|
||||
<div id="mod-center-other-options">
|
||||
<% if @community_mod_channel %>
|
||||
<a href="/connect/<%= @community_mod_channel.slug %>" target="_blank" rel="noopener noreferrer nofollow" class="crayons-link crayons-link--block">
|
||||
Connect Channel
|
||||
<span class="crayons-icon">
|
||||
<%= inline_svg_tag("external-link-logo.svg", aria: true, title: "External link") %>
|
||||
</span>
|
||||
</a>
|
||||
<% end %>
|
||||
<a href="<%= URL.url %>/community-moderation" class="crayons-link crayons-link--block">
|
||||
Moderator Guide
|
||||
<span class="crayons-icon">
|
||||
<%= inline_svg_tag("external-link-logo.svg", aria: true, title: "External link") %>
|
||||
</span>
|
||||
</a>
|
||||
<a href="<%= URL.url %>/terms" class="crayons-link crayons-link--block">
|
||||
Terms Of Conduct
|
||||
<span class="crayons-icon">
|
||||
<%= inline_svg_tag("external-link-logo.svg", aria: true, title: "External link") %>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<% if @community_mod_channel %>
|
||||
<div class="fs-s mod-feedback">
|
||||
<span class="fw-medium">Have feedback to improve your Mod experience?</span>
|
||||
<span>Please share in your Connect Channel!</span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
10
app/views/moderations/_mod_sidebar_right.html.erb
Normal file
10
app/views/moderations/_mod_sidebar_right.html.erb
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<div id="sidebar-wrapper-right" class="sidebar-wrapper sidebar-wrapper-right crayons-layout__sidebar-right">
|
||||
<div id="quick-stat-card" class="crayons-card">
|
||||
<div class="crayons-card__body mod-saying-hello">
|
||||
<p class="fw-medium fs-2xl">Hello! 👋</p>
|
||||
<p>
|
||||
Thank you for helping to keep <%= ApplicationConfig["COMMUNITY_NAME"] %> safe! ❤️
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,63 +1,26 @@
|
|||
<% title "Moderate" %>
|
||||
<% title "Mod Center" %>
|
||||
|
||||
<%= javascript_packs_with_chunks_tag "modCenter", defer: true %>
|
||||
|
||||
<div id="moderation-page"></div>
|
||||
|
||||
<% if current_user&.trusted %>
|
||||
<% if @tag %>
|
||||
<style>
|
||||
.mod-index-hero, .mod-index-header-link--active {
|
||||
background: <%= @tag.bg_color_hex %> !important;
|
||||
color: <%= @tag.text_color_hex %> !important;
|
||||
}
|
||||
.mod-index-hero .mod-index-hero-data-pod {
|
||||
border-color: <%= @tag.text_color_hex %> !important;
|
||||
}
|
||||
</style>
|
||||
<% end %>
|
||||
<% cache("mod-data-header-#{params[:tag]}", expires_in: 6.hours) do %>
|
||||
<div class="mod-index-hero">
|
||||
<h1><%= "#" + @tag.name.titleize if @tag %> Moderators</h1>
|
||||
<h2>We build the <%= community_qualified_name %></h2>
|
||||
<div class="crayons-layout crayons-layout--2-cols" id="mod-center">
|
||||
<%= render "moderations/mod_sidebar_left" %>
|
||||
<div class="mod-index-container articles-list crayons-layout__content">
|
||||
<header class="mod-index-header">
|
||||
<span class="tag-view fs-3xl fw-bold lh-base">
|
||||
<%= @tag ? "##{@tag.name}" : "All topics" %>
|
||||
</span>
|
||||
</header>
|
||||
<header class="mod-index-list-header lh-tight">
|
||||
<h2 class="fs-xs fw-medium">Post</h2>
|
||||
<h2 class="fs-xs fw-medium">Author</h2>
|
||||
<h2 class="fs-xs fw-medium">Date</h2>
|
||||
</header>
|
||||
<main class="mod-index-list" id="mod-index-list" data-articles="<%= @articles %>">
|
||||
</main>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="mod-index-header">
|
||||
<div class="mod-index-header-top-filters">
|
||||
<a href="<%= request.path %>" class="<%= "mod-index-top-filter-link--active" if params[:state].blank? %>">Base Feed</a>
|
||||
<a href="<%= request.path %>?state=new-authors" class="<%= "mod-index-top-filter-link--active" if params[:state] == "new-authors" %>">New Authors Only</a>
|
||||
</div>
|
||||
<% if current_user.moderator_for_tags.any? %>
|
||||
<a href="/mod<%= "?state=new-authors" if params[:state] == "new-authors" %>" class="mod-index-header-link mod-index-header-link--<%= "active" if params[:tag].blank? %>">Site Wide</a>
|
||||
<% current_user.moderator_for_tags.each do |tag| %>
|
||||
<a href="/mod/<%= tag %><%= "?state=new-authors" if params[:state] == "new-authors" %>" class="mod-index-header-link mod-index-header-link--<%= "active" if params[:tag] == tag %>">#<%= tag %></a>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="mod-index-list">
|
||||
<% @articles.each_with_index do |article, i| %>
|
||||
<div class="mod-index-row">
|
||||
<a href="<%= article.path %>" class="mod-index-article-title"><%= article.title %></a>
|
||||
<div class="mod-index-article-tagslist">
|
||||
<% article.cached_tag_list_array.each do |tag| %>
|
||||
<% tag_hash = tag_colors(tag) %>
|
||||
<a class="mod-index-article-tag" href="/t/<%= tag %>" style="background-color:<%= tag_hash[:background] %>;color:<%= tag_hash[:color] %>">#<%= tag %></a>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="mod-index-actions-and-details">
|
||||
<button class="quick-mod-button cta" data-path="<%= article.path %>">Quick Mod</button>
|
||||
<a href="/<%= article.cached_user.username %>"><img src="<%= ProfileImage.new(article.cached_user).get(width: 90) %>" /><%= article.cached_user.name %></a>
|
||||
・ <%= article.readable_publish_date %>
|
||||
<% if article.nth_published_by_author > 0 && article.nth_published_by_author < 4 %>
|
||||
・ 👋 <strong><%= article.nth_published_by_author.ordinalize %> post by this user</strong>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="mod-index-quick-mod" id="quick-mod-<%= i %>"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="mod-index-footer">
|
||||
<a href="/community-moderation" class="mod-link">Community Moderation Guide</a>
|
||||
<% if current_user.moderator_for_tags.any? %>
|
||||
・ <a href="<%= tag_moderation_url %>" class="mod-link">Tag Moderation Guide</a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="container" style="margin-top: 90px;">
|
||||
|
|
@ -66,24 +29,8 @@
|
|||
<p>We periodically award some <%= community_name %> members with heightened privileges to help moderate the community.</p>
|
||||
<p>Email <%= email_link %> if you'd like to be considered right away.</p>
|
||||
<% unless user_signed_in? %>
|
||||
<p><em>P.S. You are not currently signed in.</em></p>
|
||||
<p><em>P.S. You are not currently signed in.</em></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<script>
|
||||
var els = document.getElementsByClassName('quick-mod-button');
|
||||
for (let i = 0; i < els.length; i += 1) {
|
||||
els[i].onclick = function(e) {
|
||||
let path = e.target.dataset.path
|
||||
let el = document.getElementById('quick-mod-' + i)
|
||||
// if iframe is opened/loaded, clear the innerHTML
|
||||
if (el.innerHTML.length > 10) {
|
||||
el.innerHTML = ''
|
||||
} else {
|
||||
el.innerHTML = '<div class="mod-index-row-iframes"><iframe id="quick-mod-article" src="' + path + '"></iframe><iframe class="actions-panel" src="' + path + '/actions_panel"></iframe></div>'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -59,15 +59,16 @@ RSpec.describe "Moderations", type: :request do
|
|||
end
|
||||
|
||||
it "grants access to /mod index with articles" do
|
||||
create(:article, published: true)
|
||||
article = create(:article, published: true)
|
||||
get "/mod"
|
||||
expect(response.body).to include("We build the")
|
||||
expect(response.body).to include(article.title)
|
||||
end
|
||||
|
||||
it "grants access to /mod/:tag index with articles" do
|
||||
create(:article, published: true)
|
||||
get "/mod/#{article.tags.first}"
|
||||
expect(response.body).to include("#" + article.tags.first.name.titleize)
|
||||
expect(response.body).to include("#" + article.tags.first.name)
|
||||
expect(response.body).to include(article.title)
|
||||
end
|
||||
|
||||
it "returns not found for inappropriate tags" do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue