Skip to content
Merged
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
1 change: 1 addition & 0 deletions framework/core/js/src/admin/AdminApplication.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface AdminApplicationData extends ApplicationData {
displayNameDrivers: string[];
slugDrivers: Record<string, string[]>;
permissions: Record<string, string[]>;
announcementsDisabled: boolean;
}

export default class AdminApplication extends Application {
Expand Down
65 changes: 65 additions & 0 deletions framework/core/js/src/admin/components/AnnouncementItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import app from '../../admin/app';
import Component from '../../common/Component';
import icon from '../../common/helpers/icon';
import Link from '../../common/components/Link';
import dayjs from 'dayjs';

export interface AnnouncementData {
id: string;
title: string;
slug: string;
commentCount: number;
createdAt: string;
isSticky: boolean;
url: string;
excerpt: string;
authorName: string | null;
avatarUrl: string | null;
}

export interface IAnnouncementItemAttrs {
announcement: AnnouncementData;
}

export default class AnnouncementItem extends Component<IAnnouncementItemAttrs> {
view() {
const a = this.attrs.announcement;
const date = dayjs(a.createdAt).format('MMM D, YYYY');

return (
<Link className="AnnouncementItem" href={a.url} external={true} target="_blank">
<div className="AnnouncementItem-body">
<h3 className="AnnouncementItem-title">
{a.isSticky && icon('fas fa-thumbtack', { className: 'AnnouncementItem-stickyIcon' })}
{a.title}
</h3>
{a.excerpt && <p className="AnnouncementItem-excerpt">{a.excerpt}</p>}
</div>
<div className="AnnouncementItem-footer">
<div className="AnnouncementItem-byline">
{a.avatarUrl ? (
<img className="AnnouncementItem-avatar" src={a.avatarUrl} alt={a.authorName ?? ''} loading="lazy" />
) : (
<span className="AnnouncementItem-avatarFallback">{icon('fas fa-user')}</span>
)}
<div className="AnnouncementItem-bylineText">
{a.authorName && <span className="AnnouncementItem-authorName">{a.authorName}</span>}
<span className="AnnouncementItem-meta">
<span className="AnnouncementItem-date">{date}</span>
<span className="AnnouncementItem-sep">·</span>
<span className="AnnouncementItem-comments">
{icon('fas fa-comment-alt')}
{app.translator.trans('core.admin.announcements.comments_label', { count: a.commentCount })}
</span>
</span>
</div>
</div>
<div className="AnnouncementItem-readMore">
{app.translator.trans('core.admin.announcements.read_more')}
{icon('fas fa-arrow-right')}
</div>
</div>
</Link>
);
}
}
60 changes: 60 additions & 0 deletions framework/core/js/src/admin/components/AnnouncementList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Component from '../../common/Component';
import AnnouncementItem, { AnnouncementData } from './AnnouncementItem';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import icon from '../../common/helpers/icon';
import Link from '../../common/components/Link';
import app from '../../admin/app';

export interface IAnnouncementListAttrs {
announcements: AnnouncementData[] | null;
loading: boolean;
error: boolean;
onRetry: () => void;
}

export default class AnnouncementList extends Component<IAnnouncementListAttrs> {
view() {
const { announcements, loading, error, onRetry } = this.attrs;

if (loading) {
return (
<div className="AnnouncementList-state">
<LoadingIndicator />
</div>
);
}

if (error) {
return (
<div className="AnnouncementList-state AnnouncementList-state--error">
{icon('fas fa-exclamation-circle')}
<p>{app.translator.trans('core.admin.announcements.load_error')}</p>
<button className="Button" onclick={onRetry}>
{app.translator.trans('core.admin.announcements.retry')}
</button>
</div>
);
}

if (!announcements?.length) {
return (
<div className="AnnouncementList-state AnnouncementList-state--empty">
{icon('fas fa-bullhorn')}
<p>{app.translator.trans('core.admin.announcements.empty')}</p>
</div>
);
}

return (
<div className="AnnouncementList">
{announcements.map((a) => (
<AnnouncementItem key={a.id} announcement={a} />
))}
<Link className="AnnouncementList-viewAll" href="https://discuss.flarum.org/t/blog" external={true} target="_blank">
{icon('fas fa-external-link-alt')}
{app.translator.trans('core.admin.announcements.view_all')}
</Link>
</div>
);
}
}
97 changes: 97 additions & 0 deletions framework/core/js/src/admin/components/AnnouncementWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import app from '../../admin/app';
import DashboardWidget from './DashboardWidget';
import AnnouncementList from './AnnouncementList';
import type { AnnouncementData } from './AnnouncementItem';
import type { IDashboardWidgetAttrs } from './DashboardWidget';
import type Mithril from 'mithril';
import icon from '../../common/helpers/icon';
import Button from '../../common/components/Button';
import Tooltip from '../../common/components/Tooltip';

const HIDDEN_KEY = 'flarum.announcements.hidden';

export default class AnnouncementsWidget extends DashboardWidget {
announcements: AnnouncementData[] | null = null;
loadError = false;
loading = false;
hidden = false;

oninit(vnode: Mithril.Vnode<IDashboardWidgetAttrs, this>) {
super.oninit(vnode);
this.hidden = localStorage.getItem(HIDDEN_KEY) === '1';

if (!this.hidden) {
this.load();
}
}

className() {
return 'AnnouncementsWidget' + (this.hidden ? ' AnnouncementsWidget--hidden' : '');
}

async load(bust = false) {
this.loading = true;
this.loadError = false;
m.redraw();

try {
const url = app.forum.attribute('apiUrl') + '/flarum/announcements' + (bust ? '?bust=1' : '');
const data = (await app.request({ method: 'GET', url })) as unknown as AnnouncementData[];
this.announcements = data;
} catch (e) {
this.loadError = true;
} finally {
this.loading = false;
m.redraw();
}
}

toggleHidden() {
this.hidden = !this.hidden;

if (this.hidden) {
localStorage.setItem(HIDDEN_KEY, '1');
} else {
localStorage.removeItem(HIDDEN_KEY);
if (!this.announcements && !this.loading) {
this.load();
}
}

m.redraw();
}

content() {
return (
<>
<div className="AnnouncementsWidget-header">
<h2 className="AnnouncementsWidget-title">
{icon('fas fa-bullhorn')}
{app.translator.trans('core.admin.announcements.title')}
<Tooltip text={app.translator.trans('core.admin.announcements.about')}>
<span className="AnnouncementsWidget-info">{icon('fas fa-info-circle')}</span>
</Tooltip>
</h2>
<div className="AnnouncementsWidget-controls">
{!this.hidden && (
<Tooltip text={app.translator.trans('core.admin.announcements.refresh')}>
<Button
className="Button Button--icon"
icon={this.loading ? 'fas fa-sync-alt fa-spin' : 'fas fa-sync-alt'}
disabled={this.loading}
onclick={() => this.load(true)}
/>
</Tooltip>
)}
<Tooltip text={app.translator.trans(this.hidden ? 'core.admin.announcements.show' : 'core.admin.announcements.hide')}>
<Button className="Button Button--icon" icon={this.hidden ? 'fas fa-eye' : 'fas fa-eye-slash'} onclick={() => this.toggleHidden()} />
</Tooltip>
</div>
</div>
{!this.hidden && (
<AnnouncementList announcements={this.announcements} loading={this.loading} error={this.loadError} onRetry={() => this.load()} />
)}
</>
);
}
}
5 changes: 5 additions & 0 deletions framework/core/js/src/admin/components/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ItemList from '../../common/utils/ItemList';
import AdminPage from './AdminPage';
import type { Children } from 'mithril';
import DebugWarningWidget from './DebugWarningWidget';
import AnnouncementsWidget from './AnnouncementWidget';

export default class DashboardPage extends AdminPage {
headerInfo() {
Expand All @@ -31,6 +32,10 @@ export default class DashboardPage extends AdminPage {

items.add('extensions', <ExtensionsWidget />, 10);

if (!app.data.announcementsDisabled) {
items.add('announcements', <AnnouncementsWidget />, 29);
}

return items;
}
}
1 change: 1 addition & 0 deletions framework/core/less/admin.less
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
@import "admin/MailPage";
@import "admin/NoJs";
@import "admin/UsersListPage";
@import "admin/AnnouncementsPage";
Loading
Loading