Super MVP-ish annual report page

This commit is contained in:
Lim Chee Aun 2024-12-04 01:55:25 +08:00
parent ab6a977aeb
commit 4270304a28
5 changed files with 258 additions and 19 deletions

View file

@ -46,6 +46,7 @@ import Search from './pages/search';
import StatusRoute from './pages/status-route';
import Trending from './pages/trending';
import Welcome from './pages/welcome';
import AnnualReport from './pages/annual-report';
import {
api,
hasInstance,
@ -546,6 +547,7 @@ function SecondaryRoutes({ isLoggedIn }) {
<Route path="/fh" element={<FollowedHashtags />} />
<Route path="/ft" element={<Filters />} />
<Route path="/catchup" element={<Catchup />} />
<Route path="/annual_report/:year" element={<AnnualReport />} />
</>
)}
<Route path="/:instance?/t/:hashtag" element={<Hashtag />} />

View file

@ -261,6 +261,9 @@ const contentText = {
),
emoji_reaction: emojiText,
'pleroma:emoji_reaction': emojiText,
annual_report: ({ year }) => (
<Trans>Your {year} #Wrapstodon is here!</Trans>
),
};
// account_suspension, domain_block, user_domain_block
@ -312,6 +315,7 @@ function Notification({
report,
event,
moderation_warning,
annualReport,
// Client-side grouped notification
_ids,
_accounts,
@ -409,6 +413,10 @@ function Notification({
emoji: notification.emoji,
emojiURL,
});
} else if (type === 'annual_report') {
text = text({
...notification.annualReport,
});
} else {
text = text({
account: account ? (
@ -527,6 +535,11 @@ function Notification({
</a>
</div>
)}
{type === 'annual_report' && (
<div>
<Link to={`/annual_report/${annualReport?.year}`}><Trans>View #Wrapstodon</Trans></Link>
</div>
)}
</>
)}
{_accounts?.length > 1 && (

48
src/locales/en.po generated
View file

@ -963,7 +963,7 @@ msgid "Nothing to show"
msgstr ""
#: src/components/generic-accounts.jsx:145
#: src/components/notification.jsx:438
#: src/components/notification.jsx:446
#: src/pages/accounts.jsx:41
#: src/pages/search.jsx:317
#: src/pages/search.jsx:350
@ -1472,74 +1472,82 @@ msgstr ""
msgid "Moderation warning"
msgstr ""
#: src/components/notification.jsx:269
#: src/components/notification.jsx:265
msgid "Your {year} #Wrapstodon is here!"
msgstr "Your {year} #Wrapstodon is here!"
#: src/components/notification.jsx:272
msgid "An admin from <0>{from}</0> has suspended <1>{targetName}</1>, which means you can no longer receive updates from them or interact with them."
msgstr ""
#: src/components/notification.jsx:275
#: src/components/notification.jsx:278
msgid "An admin from <0>{from}</0> has blocked <1>{targetName}</1>. Affected followers: {followersCount}, followings: {followingCount}."
msgstr ""
#: src/components/notification.jsx:281
#: src/components/notification.jsx:284
msgid "You have blocked <0>{targetName}</0>. Removed followers: {followersCount}, followings: {followingCount}."
msgstr ""
#: src/components/notification.jsx:289
#: src/components/notification.jsx:292
msgid "Your account has received a moderation warning."
msgstr ""
#: src/components/notification.jsx:290
#: src/components/notification.jsx:293
msgid "Your account has been disabled."
msgstr ""
#: src/components/notification.jsx:291
#: src/components/notification.jsx:294
msgid "Some of your posts have been marked as sensitive."
msgstr ""
#: src/components/notification.jsx:292
#: src/components/notification.jsx:295
msgid "Some of your posts have been deleted."
msgstr ""
#: src/components/notification.jsx:293
#: src/components/notification.jsx:296
msgid "Your posts will be marked as sensitive from now on."
msgstr ""
#: src/components/notification.jsx:294
#: src/components/notification.jsx:297
msgid "Your account has been limited."
msgstr ""
#: src/components/notification.jsx:295
#: src/components/notification.jsx:298
msgid "Your account has been suspended."
msgstr ""
#: src/components/notification.jsx:369
#: src/components/notification.jsx:373
msgid "[Unknown notification type: {type}]"
msgstr ""
#: src/components/notification.jsx:434
#: src/components/notification.jsx:442
#: src/components/status.jsx:1036
#: src/components/status.jsx:1046
msgid "Boosted/Liked by…"
msgstr ""
#: src/components/notification.jsx:435
#: src/components/notification.jsx:443
msgid "Liked by…"
msgstr ""
#: src/components/notification.jsx:436
#: src/components/notification.jsx:444
msgid "Boosted by…"
msgstr ""
#: src/components/notification.jsx:437
#: src/components/notification.jsx:445
msgid "Followed by…"
msgstr ""
#: src/components/notification.jsx:508
#: src/components/notification.jsx:524
#: src/components/notification.jsx:516
#: src/components/notification.jsx:532
msgid "Learn more <0/>"
msgstr ""
#: src/components/notification.jsx:756
#: src/components/notification.jsx:540
msgid "View #Wrapstodon"
msgstr "View #Wrapstodon"
#: src/components/notification.jsx:769
#: src/components/status.jsx:267
msgid "Read more →"
msgstr ""
@ -2260,6 +2268,7 @@ msgid "Failed to load history"
msgstr ""
#: src/components/status.jsx:3006
#: src/pages/annual-report.jsx:44
msgid "Loading…"
msgstr ""
@ -2401,6 +2410,7 @@ msgid "Login required."
msgstr "Login required."
#: src/compose.jsx:90
#: src/pages/annual-report.jsx:132
#: src/pages/http-route.jsx:91
#: src/pages/login.jsx:270
msgid "Go home"

View file

@ -0,0 +1,77 @@
#annual-report-page {
.report {
background-color: var(--bg-color);
border: 16px ridge var(--bg-faded-color);
box-shadow: 0 0 0 2px var(--bg-color);
padding: 16px;
margin: 80px auto;
max-width: var(--main-width);
font-family: var(--monospace-font);
font-variant-numeric: slashed-zero;
font-feature-settings: 'ss01';
font-variant-numeric: tabular-nums;
min-height: 80vh;
h1 {
margin: 0;
padding: 0;
}
dt {
font-weight: bold;
font-size: larger;
}
dd {
margin: 0 0 2em;
padding: 0;
overflow: auto;
}
table {
width: 100%;
td, th {
vertical-align: top;
}
th {
font-weight: normal;
text-align: start;
color: var(--text-insignificant-color);
text-transform: uppercase;
}
tr > * {
border-top: 1px dashed var(--outline-color);
}
}
.report-topStatuses {
dt {
font-size: var(--text-size);
}
dd {
margin-block-end: 1em;
> a {
display: block;
color: inherit;
text-decoration: none;
border: 2px dashed var(--outline-stronger-color);
&:is(:hover, :focus) {
border-color: var(--text-color);
}
}
.status {
pointer-events: none;
font-size: calc(var(--text-size) * .8);
}
}
}
}
}

137
src/pages/annual-report.jsx Normal file
View file

@ -0,0 +1,137 @@
import { t, Trans } from '@lingui/macro';
import './annual-report.css';
import { useEffect, useState } from 'preact/hooks';
import { useParams } from 'react-router-dom';
import Link from '../components/link';
import Loader from '../components/loader';
import NameText from '../components/name-text';
import Status from '../components/status';
import { api } from '../utils/api';
import useTitle from '../utils/useTitle';
export default function AnnualReport() {
const params = useParams();
const { year } = params;
useTitle(year ? `Annual Report: ${year}` : 'Annual Report');
const { masto, instance } = api();
const [results, setResults] = useState(null);
const [uiState, setUIState] = useState('default');
useEffect(() => {
if (year) {
(async () => {
setUIState('loading');
const results = await masto.v1.annualReports.$select(year).fetch();
console.log('REPORT', results);
setResults(results);
setUIState('default');
})();
}
}, [year]);
const { accounts, annualReports, statuses } = results || {};
const report = annualReports?.find((report) => report.year == year)?.data;
return (
<div id="annual-report-page" class="deck-container" tabIndex="-1">
<div class="report">
<h1>{year} #Wrapstodon</h1>
{uiState === 'loading' && (
<p>
<Loader abrupt /> <Trans>Loading</Trans>
</p>
)}
{!!report && (
<dl>
{Object.entries(report).map(([key, value]) => (
<>
<dt>{key}</dt>
<dd class={`report-${key}`}>
{Array.isArray(value) ? (
<table>
<thead>
<tr>
{Object.keys(value[0]).map((key) => (
<th>{key}</th>
))}
</tr>
</thead>
<tbody>
{value.map((item) => (
<tr>
{Object.entries(item).map(([k, value]) => (
<td>
{value && /(accountId)/i.test(k) &&
/^(mostRebloggedAccounts|commonlyInteractedWithAccounts)$/i.test(
key,
) ? (
<NameText
account={accounts?.find(
(a) => a.id === value,
)}
showAvatar
/>
) : (
value
)}
</td>
))}
</tr>
))}
</tbody>
</table>
) : typeof value === 'object' ? (
/^(topStatuses)$/i.test(key) ? (
<dl>
{Object.entries(value).map(([k, value]) => (
<>
<dt>{k}</dt>
<dd>
{value &&
<Link to={`/${instance}/s/${value}`}>
<Status
status={statuses?.find((s) => s.id === value)}
size="s"
readOnly
/>
</Link>}
</dd>
</>
))}
</dl>
) : (
<table>
<tbody>
{Object.entries(value).map(([k, value]) => (
<tr>
<th>{k}</th>
<td>{value}</td>
</tr>
))}
</tbody>
</table>
)
) : typeof value === 'string' ? (
value
) : (
// Last resort
JSON.stringify(value, null, 2)
)}
</dd>
</>
))}
</dl>
)}
</div>
<hr />
<p style={{ textAlign: 'center' }}>
<Link to="/">
<Trans>Go home</Trans>
</Link>
</p>
</div>
);
}