Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Di/polls UI prototype #87

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
return [
(new Extend\Frontend('forum'))
->js(__DIR__.'/js/dist/forum.js')
->css(__DIR__.'/resources/less/forum.less'),
->css(__DIR__.'/resources/less/forum.less')
->route('/polls', 'polls'),

(new Extend\Frontend('admin'))
->js(__DIR__.'/js/dist/admin.js')
Expand Down
55 changes: 55 additions & 0 deletions js/src/common/models/Poll.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import app from 'flarum/forum/app';
import Model from 'flarum/common/Model';
import User from 'flarum/common/Model';
import computed from 'flarum/common/utils/computed';

export default class Poll extends Model {
title() {
return Model.attribute<string>('title').call(this);
}
slug() {
return Model.attribute<string>('slug').call(this);
}

createdAt() {
return Model.attribute<Date | undefined, string | undefined>('createdAt', Model.transformDate).call(this);
}
user() {
return Model.hasOne<User | null>('user').call(this);
}

voteCount() {
return Model.attribute<number>('voteCount').call(this);
}

// TODO: These two don't make sense as of now
isUnread() {
return computed<boolean, this>('unreadCount', (unreadCount) => !!unreadCount).call(this);
}
isRead() {
return computed<boolean, this>('unreadCount', (unreadCount) => !!(app.session.user && !unreadCount)).call(this);
}

hiddenAt() {
return Model.attribute('hiddenAt', Model.transformDate).call(this);
}
hiddenUser() {
return Model.hasOne<User | null>('hiddenUser').call(this);
}
isHidden() {
return computed<boolean, this>('hiddenAt', (hiddenAt) => !!hiddenAt).call(this);
}

canVote() {
return Model.attribute<boolean | undefined>('canVote').call(this);
}
canRename() {
return Model.attribute<boolean | undefined>('canRename').call(this);
}
canHide() {
return Model.attribute<boolean | undefined>('canHide').call(this);
}
canDelete() {
return Model.attribute<boolean | undefined>('canDelete').call(this);
}
}
17 changes: 17 additions & 0 deletions js/src/forum/addPollsLink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { extend } from 'flarum/common/extend';
import IndexPage from 'flarum/forum/components/IndexPage';
import LinkButton from 'flarum/common/components/LinkButton';

import app from 'flarum/forum/app';

export default function () {
extend(IndexPage.prototype, 'navItems', function (items) {
items.add(
'polls',
<LinkButton icon="fas fa-poll" href={app.route('polls')}>
Polls
</LinkButton>,
-11
);
});
}
33 changes: 33 additions & 0 deletions js/src/forum/components/Poll.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as Mithril from 'mithril';
import Component from 'flarum/common/Component';

import PollTitle from './Poll/PollTitle';
import PollDescription from './Poll/PollDescription';
import PollOptions from './Poll/PollOptions';
import PollSubmitButton from './Poll/PollSubmitButton';
import PollImage from './Poll/PollImage';

export default class IndexPolls extends Component {
view(): Mithril.Children {
return (
<div className="Poll">
<div className="Poll-image">
<PollImage />
</div>
<div className="Poll-wrapper">
<PollTitle text="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy?" />
<PollDescription text="At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet!" />

<form>
<fieldset>
<legend className="sr-only">Privacy setting</legend>

<PollOptions />
<PollSubmitButton />
</fieldset>
</form>
</div>
</div>
);
}
}
12 changes: 12 additions & 0 deletions js/src/forum/components/Poll/PollDescription.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as Mithril from 'mithril';
import Component, { ComponentAttrs } from 'flarum/common/Component';

interface PollDescriptionAttrs extends ComponentAttrs {
text: String;
}

export default class PollDescription extends Component<PollDescriptionAttrs> {
view(): Mithril.Children {
return <p className="Poll-description">{this.attrs.text}</p>;
}
}
8 changes: 8 additions & 0 deletions js/src/forum/components/Poll/PollImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as Mithril from 'mithril';
import Component from 'flarum/common/Component';

export default class PollImage extends Component {
view(): Mithril.Children {
return;
}
}
64 changes: 64 additions & 0 deletions js/src/forum/components/Poll/PollList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import app from 'flarum/forum/app';
import Component from 'flarum/common/Component';
import PollListItem from './PollListItem';
import Button from 'flarum/common/components/Button';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
import Placeholder from 'flarum/common/components/Placeholder';
import classList from 'flarum/common/utils/classList';

/**
* The `PollList` component displays a list of polls.
*/
export default class PollList extends Component {
view() {
/**
* @type {import('../../states/PollListState').default}
*/
const state = this.attrs.state;

const params = state.getParams();
const isLoading = state.isInitialLoading() || state.isLoadingNext();

let loading;

if (isLoading) {
loading = <LoadingIndicator />;
} else if (state.hasNext()) {
loading = (
<Button className="Button" onclick={state.loadNext.bind(state)}>
{/* TODO: IS THIS THE RIGHT KEY? MAYBE WE SHOULD MAKE A NEW ONE? */}
{app.translator.trans('core.forum.discussion_list.load_more_button')}
</Button>
);
}

if (state.isEmpty()) {
{
/* TODO: IS THIS THE RIGHT KEY? MAYBE WE SHOULD MAKE A NEW ONE? */
}
const text = app.translator.trans('core.forum.discussion_list.empty_text');
return (
<div className="PollList">
<Placeholder text={text} />
</div>
);
}

const pageSize = state.pageSize;

return (
<div className={classList('PollList', { 'PollList--searchResults': state.isSearchResults() })}>
<ul aria-busy={isLoading} className="PollList-polls">
{state.getPages().map((pg) => {
return pg.items.map((poll) => (
<li key={poll.id()} data-id={poll.id()}>
<PollListItem poll={poll} params={params} />
</li>
));
})}
</ul>
<div className="PollList-loadMore">{loading}</div>
</div>
);
}
}
193 changes: 193 additions & 0 deletions js/src/forum/components/Poll/PollListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import * as Mithril from 'mithril';
import app from 'flarum/forum/app';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import type Poll from '../../../common/models/Poll';
import type { PollListParams } from '../../states/PollListState';
import SubtreeRetainer from 'flarum/common/utils/SubtreeRetainer';
import classList from 'flarum/common/utils/classList';
import Dropdown from 'flarum/common/components/Dropdown';
import Link from 'flarum/common/components/Link';
import highlight from 'flarum/common/helpers/highlight';
import slidable from 'flarum/forum/utils/slidable';
import icon from 'flarum/common/helpers/icon';
import PollPage from './PollPage';
import abbreviateNumber from 'flarum/common/utils/abbreviateNumber';

export interface IPollListItemAttrs extends ComponentAttrs {
poll: Poll;
params: PollListParams;
}

/**
* The `PollListItem` component shows a single poll in the
* poll list.
*/
export default class PollListItem<CustomAttrs extends IPollListItemAttrs = IPollListItemAttrs> extends Component<CustomAttrs> {
/**
* Ensures that the poll will not be redrawn
* unless new data comes in.
*/
subtree!: SubtreeRetainer;

highlightRegExp?: RegExp;

oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);

this.subtree = new SubtreeRetainer(
() => this.attrs.poll.freshness,
() => {
const time = app.session.user && app.session.user.markedAllAsReadAt();
return time && time.getTime();
},
() => this.active()
);
}

elementAttrs() {
return {
className: classList('PollListItem', {
active: this.active(),
'PollListItem--hidden': this.attrs.poll.isHidden(),
Slidable: 'ontouchstart' in window,
}),
};
}

view() {
const poll = this.attrs.poll;

// TODO IMPLEMENT POLLCONTROLS
//const controls = PollControls.controls(poll, this).toArray();
const attrs = this.elementAttrs();

return (
<div {...attrs}>
{/* {this.controlsView(controls)} */}
{this.contentView()}
{this.slidableUnderneathView()}
</div>
);
}

controlsView(controls: Mithril.ChildArray): Mithril.Children {
return (
!!controls.length && (
<Dropdown
icon="fas fa-ellipsis-v"
className="PollListItem-controls"
buttonClassName="Button Button--icon Button--flat"
accessibleToggleLabel={app.translator.trans('fof-polls.forum.poll_controls.toggle_dropdown_accessible_label')}
>
{controls}
</Dropdown>
)
);
}

slidableUnderneathView(): Mithril.Children {
const poll = this.attrs.poll;
const isUnread = poll.isUnread();

return (
<span
className={classList('Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic', { disabled: !isUnread })}
onclick={this.markAsRead.bind(this)}
>
{icon('fas fa-check')}
</span>
);
}

contentView(): Mithril.Children {
const poll = this.attrs.poll;
// const isUnread = poll.isUnread();
// const isRead = poll.isRead();

return (
// <div className={classList('PollListItem-content', 'Slidable-content', { unread: isUnread, read: isRead })}>
<div className={classList('PollListItem-content')}>
{/* {this.authorAvatarView()}
{this.badgesView()} */}
{this.mainView()}
{this.voteCountItem()}
</div>
);
}

mainView(): Mithril.Children {
const poll = this.attrs.poll;

return (
<Link href={app.route.poll(poll)} className="PollListItem-main">
<h2 className="PollListItem-title">{highlight(poll.title(), this.highlightRegExp)}</h2>
{/* <ul className="PollListItem-info">{listItems(this.infoItems().toArray())}</ul> */}
</Link>
);
}

oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
super.oncreate(vnode);

// If we're on a touch device, set up the discussion row to be slidable.
// This allows the user to drag the row to either side of the screen to
// reveal controls.
if ('ontouchstart' in window) {
const slidableInstance = slidable(this.element);

this.$('.PollListItem-controls').on('hidden.bs.dropdown', () => slidableInstance.reset());
}
}

onbeforeupdate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
super.onbeforeupdate(vnode);

return this.subtree.needsRebuild();
}

/**
* Determine whether or not the discussion is currently being viewed.
*/
active() {
return app.current.matches(PollPage, { poll: this.attrs.poll });
}

/**
* Mark the poll as read.
*/
markAsRead() {
const poll = this.attrs.poll;

if (poll.isUnread()) {
poll.save({ lastVotedNumber: poll.voteCount() });
m.redraw();
}
}

voteCountItem() {
const poll = this.attrs.poll;
const isUnread = poll.isUnread();

if (isUnread) {
return (
<button className="Button--ua-reset PollListItem-count" onclick={this.markAsRead.bind(this)}>
<span aria-hidden="true">{abbreviateNumber(poll.voteCount())}</span>

<span className="visually-hidden">
{app.translator.trans('core.forum.discussion_list.unread_replies_a11y_label', { count: poll.voteCount() })}
</span>
</button>
);
}

return (
<span className="PollListItem-count">
<span aria-hidden="true">{abbreviateNumber(poll.voteCount())}</span>

<span className="visually-hidden">
{app.translator.trans('fof-polls.forum.poll_list.total_votes_a11y_label', { count: poll.voteCount() })}
</span>
</span>
);
}
}
Loading
Loading