+
+
+
-
-
-
-
-
- {change > -1 ? '+' : '-'}
-
-
-
-
+
+
);
diff --git a/app/javascript/flavours/glitch/features/annual_report/highlighted_post.tsx b/app/javascript/flavours/glitch/features/annual_report/highlighted_post.tsx
index 6d23e5deb6..61fa365e72 100644
--- a/app/javascript/flavours/glitch/features/annual_report/highlighted_post.tsx
+++ b/app/javascript/flavours/glitch/features/annual_report/highlighted_post.tsx
@@ -1,106 +1,77 @@
/* eslint-disable @typescript-eslint/no-unsafe-return,
@typescript-eslint/no-explicit-any,
- @typescript-eslint/no-unsafe-assignment */
-
-import { useCallback } from 'react';
+ @typescript-eslint/no-unsafe-assignment,
+ @typescript-eslint/no-unsafe-member-access,
+ @typescript-eslint/no-unsafe-call */
import { FormattedMessage } from 'react-intl';
-import { DisplayName } from '@/flavours/glitch/components/display_name';
-import { toggleStatusSpoilers } from 'flavours/glitch/actions/statuses';
-import { DetailedStatus } from 'flavours/glitch/features/status/components/detailed_status';
-import { me } from 'flavours/glitch/initial_state';
+import classNames from 'classnames';
+
+import { StatusQuoteManager } from 'flavours/glitch/components/status_quoted';
import type { TopStatuses } from 'flavours/glitch/models/annual_report';
-import {
- makeGetStatus,
- makeGetPictureInPicture,
-} from 'flavours/glitch/selectors';
-import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
+import { makeGetStatus } from 'flavours/glitch/selectors';
+import { useAppSelector } from 'flavours/glitch/store';
+
+import styles from './index.module.scss';
const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any;
-const getPictureInPicture = makeGetPictureInPicture() as unknown as (
- arg0: any,
- arg1: any,
-) => any;
export const HighlightedPost: React.FC<{
data: TopStatuses;
}> = ({ data }) => {
- let statusId, label;
+ const { by_reblogs, by_favourites, by_replies } = data;
- if (data.by_reblogs) {
- statusId = data.by_reblogs;
- label = (
-
- );
- } else if (data.by_favourites) {
- statusId = data.by_favourites;
- label = (
-
- );
- } else {
- statusId = data.by_replies;
- label = (
-
- );
- }
+ const statusId = by_reblogs || by_favourites || by_replies;
- const dispatch = useAppDispatch();
- const domain = useAppSelector((state) => state.meta.get('domain'));
const status = useAppSelector((state) =>
statusId ? getStatus(state, { id: statusId }) : undefined,
);
- const pictureInPicture = useAppSelector((state) =>
- statusId ? getPictureInPicture(state, { id: statusId }) : undefined,
- );
- const account = useAppSelector((state) =>
- me ? state.accounts.get(me) : undefined,
- );
-
- const handleToggleHidden = useCallback(() => {
- dispatch(toggleStatusSpoilers(statusId));
- }, [dispatch, statusId]);
if (!status) {
- return (
-
+ return
;
+ }
+
+ let label;
+ if (by_reblogs) {
+ label = (
+
+ );
+ } else if (by_favourites) {
+ label = (
+
+ );
+ } else {
+ label = (
+
);
}
- const displayName = (
-
-
- ,
- }}
- />
-
- {label}
-
- );
-
return (
-
-
+
);
};
diff --git a/app/javascript/flavours/glitch/features/annual_report/index.module.scss b/app/javascript/flavours/glitch/features/annual_report/index.module.scss
new file mode 100644
index 0000000000..0258f9c798
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/annual_report/index.module.scss
@@ -0,0 +1,283 @@
+.modalWrapper {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 40px;
+ overflow-y: auto;
+ pointer-events: none;
+ scrollbar-color: var(--color-text-secondary) var(--color-bg-secondary);
+
+ .loading-indicator .circular-progress {
+ color: var(--lime);
+ }
+}
+
+.closeButton {
+ position: absolute;
+ top: 24px;
+ right: 24px;
+ padding: 8px;
+ border-radius: 100%;
+
+ --default-icon-color: var(--color-bg-primary);
+ --default-bg-color: var(--color-text-primary);
+ --hover-icon-color: var(--color-bg-primary);
+ --hover-bg-color: var(--color-text-primary);
+}
+
+.wrapper {
+ position: relative;
+ max-width: 600px;
+ padding: 24px;
+ contain: layout;
+ flex: 0 0 auto;
+ pointer-events: auto;
+ color: var(--color-text-primary);
+ background: var(--color-bg-primary);
+ background:
+ radial-gradient(at 40% 87%, #240c9a99 0, transparent 50%),
+ radial-gradient(at 19% 10%, #6b0c9a99 0, transparent 50%),
+ radial-gradient(at 90% 27%, #9a0c8299 0, transparent 50%),
+ radial-gradient(at 16% 95%, #1e948299 0, transparent 50%),
+ radial-gradient(at 80% 91%, #16dae499 0, transparent 50%)
+ var(--color-bg-primary);
+ border-radius: 40px;
+
+ @media (width < 600px) {
+ padding: 12px;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ z-index: -1;
+ background: inherit;
+ border-radius: inherit;
+ filter: blur(20px);
+ }
+}
+
+.header {
+ margin-bottom: 18px;
+ text-align: center;
+
+ h1 {
+ font-family: monospace;
+ text-transform: uppercase;
+ letter-spacing: 0.15em;
+ font-size: 30px;
+ font-weight: 600;
+ line-height: 1.5;
+ margin-bottom: 8px;
+ }
+
+ p {
+ font-size: 14px;
+ line-height: 1.5;
+ }
+}
+
+.stack {
+ --grid-spacing: 12px;
+
+ display: grid;
+ gap: var(--grid-spacing);
+}
+
+.box {
+ position: relative;
+ padding: 16px;
+ border-radius: 16px;
+ background: rgb(from var(--color-bg-primary) r g b / 60%);
+ box-shadow: inset 0 0 0 1px rgb(from var(--color-text-primary) r g b / 40%);
+
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ inset-inline: 0;
+ display: block;
+ height: 1px;
+ background-image: linear-gradient(
+ to right,
+ transparent,
+ white,
+ transparent
+ );
+ }
+
+ &::before {
+ top: 0;
+ }
+
+ &::after {
+ bottom: 0;
+ }
+}
+
+.content {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 8px;
+ padding: 16px;
+ font-size: 14px;
+ text-align: center;
+ text-wrap: balance;
+
+ &.comfortable {
+ gap: 12px;
+ }
+}
+
+.title {
+ text-transform: uppercase;
+ color: #c2c8ff;
+ font-weight: 500;
+}
+
+.statLarge {
+ font-size: 24px;
+ font-weight: 500;
+ overflow-wrap: break-word;
+}
+
+.statExtraLarge {
+ font-size: 32px;
+ font-weight: 500;
+ line-height: 1;
+ overflow-wrap: break-word;
+}
+
+.mostBoostedPost {
+ padding: 0;
+ padding-top: 8px;
+ overflow: hidden;
+}
+
+.statsGrid {
+ display: grid;
+ gap: var(--grid-spacing);
+ grid-template-columns: 1fr 2fr;
+ grid-template-areas:
+ 'followers hashtag'
+ 'new-posts hashtag';
+
+ @media (width < 680px) {
+ grid-template-columns: 1fr 1fr;
+ grid-template-areas:
+ 'followers new-posts'
+ 'hashtag hashtag';
+ }
+
+ &:empty {
+ display: none;
+ }
+
+ &.onlyHashtag {
+ grid-template-columns: 1fr;
+ grid-template-areas: 'hashtag';
+ }
+
+ &.noHashtag {
+ grid-template-columns: 1fr 1fr;
+ grid-template-areas: 'followers new-posts';
+ }
+
+ &.singleNumber {
+ grid-template-columns: 1fr 2fr;
+ grid-template-areas: 'number hashtag';
+
+ @media (width < 680px) {
+ grid-template-areas:
+ 'number number'
+ 'hashtag hashtag';
+ }
+ }
+
+ &.singleNumber.noHashtag {
+ grid-template-columns: 1fr;
+ grid-template-areas: 'number';
+ }
+}
+
+.followers {
+ grid-area: followers;
+
+ .singleNumber & {
+ grid-area: number;
+ }
+}
+
+.newPosts {
+ grid-area: new-posts;
+
+ .singleNumber & {
+ grid-area: number;
+ }
+}
+
+.mostUsedHashtag {
+ grid-area: hashtag;
+ padding-block: 24px;
+}
+
+.archetype {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+}
+
+.archetypeArtboard {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ align-self: center;
+ width: 180px;
+ padding-top: 40px;
+}
+
+.archetypeAvatar {
+ position: absolute;
+ top: 7px;
+ left: 4px;
+ border-radius: 100%;
+ overflow: hidden;
+}
+
+.archetypeIllustrationWrapper {
+ position: relative;
+ width: 92px;
+ aspect-ratio: 1;
+ overflow: hidden;
+ border-radius: 100%;
+
+ &::after {
+ content: '';
+ display: block;
+ position: absolute;
+ inset: 0;
+ border-radius: inherit;
+ box-shadow: inset -10px -4px 15px #00000080;
+ }
+}
+
+.archetypeIllustration {
+ width: 100%;
+}
+
+.blurredImage {
+ filter: blur(10px);
+}
+
+.archetypePlanetRing {
+ position: absolute;
+ top: 0;
+ left: 0;
+ mix-blend-mode: screen;
+}
diff --git a/app/javascript/flavours/glitch/features/annual_report/index.tsx b/app/javascript/flavours/glitch/features/annual_report/index.tsx
index 954c45bbda..b759610b46 100644
--- a/app/javascript/flavours/glitch/features/annual_report/index.tsx
+++ b/app/javascript/flavours/glitch/features/annual_report/index.tsx
@@ -1,95 +1,123 @@
-import { useCallback } from 'react';
+import { useCallback, useEffect, useState } from 'react';
import type { FC } from 'react';
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
-import { focusCompose, resetCompose } from '@/flavours/glitch/actions/compose';
+import { useLocation } from 'react-router';
+
+import classNames from 'classnames/bind';
+
import { closeModal } from '@/flavours/glitch/actions/modal';
-import { Button } from '@/flavours/glitch/components/button';
+import { IconButton } from '@/flavours/glitch/components/icon_button';
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
import { me } from '@/flavours/glitch/initial_state';
-import type { AnnualReport as AnnualReportData } from '@/flavours/glitch/models/annual_report';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
+import CloseIcon from '@/material-icons/400-24px/close.svg?react';
-import { Archetype, archetypeNames } from './archetype';
+import { Archetype } from './archetype';
import { Followers } from './followers';
import { HighlightedPost } from './highlighted_post';
+import styles from './index.module.scss';
import { MostUsedHashtag } from './most_used_hashtag';
import { NewPosts } from './new_posts';
-const shareMessage = defineMessage({
+const moduleClassNames = classNames.bind(styles);
+
+export const shareMessage = defineMessage({
id: 'annual_report.summary.share_message',
defaultMessage: 'I got the {archetype} archetype!',
});
// Share = false when using the embedded version of the report.
-export const AnnualReport: FC<{ share?: boolean }> = ({ share = true }) => {
- const currentAccount = useAppSelector((state) =>
- me ? state.accounts.get(me) : undefined,
- );
+export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
+ context = 'standalone',
+}) => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
const report = useAppSelector((state) => state.annualReport.report);
+ const account = useAppSelector((state) => {
+ if (me) {
+ return state.accounts.get(me);
+ }
+ if (report?.schema_version === 2) {
+ return state.accounts.get(report.account_id);
+ }
+ return undefined;
+ });
+
+ const close = useCallback(() => {
+ dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
+ }, [dispatch]);
+
+ // Close modal when navigating away from within
+ const { pathname } = useLocation();
+ const [initialPathname] = useState(pathname);
+ useEffect(() => {
+ if (pathname !== initialPathname) {
+ close();
+ }
+ }, [pathname, initialPathname, close]);
if (!report) {
return
;
}
+ const newPostCount = report.data.time_series.reduce(
+ (sum, item) => sum + item.statuses,
+ 0,
+ );
+
+ const newFollowerCount = report.data.time_series.reduce(
+ (sum, item) => sum + item.followers,
+ 0,
+ );
+
+ const topHashtag = report.data.top_hashtags[0];
+
return (
-
-
+
+
-
-
-
-
+
+ {account &&
@{account.acct}
}
+ {context === 'modal' && (
+
+ )}
-
-
+
-
+ {!!newFollowerCount && }
+ {!!newPostCount && }
+ {topHashtag && }
+
+
-
-
- {share &&
}
);
};
-
-const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
- const handleShareClick = useCallback(() => {
- // Generate the share message.
- const archetypeName = intl.formatMessage(
- archetypeNames[report.data.archetype],
- );
- const shareLines = [
- intl.formatMessage(shareMessage, {
- archetype: archetypeName,
- }),
- ];
- // Share URL is only available for schema version 2.
- if (report.schema_version === 2 && report.share_url) {
- shareLines.push(report.share_url);
- }
- shareLines.push(`#Wrapstodon${report.year}`);
-
- // Reset the composer and focus it with the share message, then close the modal.
- dispatch(resetCompose());
- dispatch(focusCompose(shareLines.join('\n\n')));
- dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
- }, [report, intl, dispatch]);
-
- return
;
-};
diff --git a/app/javascript/flavours/glitch/features/annual_report/modal.tsx b/app/javascript/flavours/glitch/features/annual_report/modal.tsx
new file mode 100644
index 0000000000..262584b7b9
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/annual_report/modal.tsx
@@ -0,0 +1,29 @@
+import { useEffect } from 'react';
+
+import classNames from 'classnames';
+
+import { AnnualReport } from '.';
+import styles from './index.module.scss';
+
+const AnnualReportModal: React.FC<{
+ onChangeBackgroundColor: (color: string) => void;
+}> = ({ onChangeBackgroundColor }) => {
+ useEffect(() => {
+ onChangeBackgroundColor('var(--color-bg-media-base)');
+ }, [onChangeBackgroundColor]);
+
+ return (
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default AnnualReportModal;
diff --git a/app/javascript/flavours/glitch/features/annual_report/most_used_hashtag.tsx b/app/javascript/flavours/glitch/features/annual_report/most_used_hashtag.tsx
index 6bf7493960..294b7460d4 100644
--- a/app/javascript/flavours/glitch/features/annual_report/most_used_hashtag.tsx
+++ b/app/javascript/flavours/glitch/features/annual_report/most_used_hashtag.tsx
@@ -1,30 +1,34 @@
import { FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+
import type { NameAndCount } from 'flavours/glitch/models/annual_report';
-export const MostUsedHashtag: React.FC<{
- data: NameAndCount[];
-}> = ({ data }) => {
- const hashtag = data[0];
+import styles from './index.module.scss';
+export const MostUsedHashtag: React.FC<{
+ hashtag: NameAndCount;
+}> = ({ hashtag }) => {
return (
-
-
- {hashtag ? (
- <>#{hashtag.name}>
- ) : (
-
- )}
-
-
+
+
+
+
#{hashtag.name}
+
+
+
+
);
};
diff --git a/app/javascript/flavours/glitch/features/annual_report/new_posts.tsx b/app/javascript/flavours/glitch/features/annual_report/new_posts.tsx
index 4ca286debb..9a265f0b9d 100644
--- a/app/javascript/flavours/glitch/features/annual_report/new_posts.tsx
+++ b/app/javascript/flavours/glitch/features/annual_report/new_posts.tsx
@@ -1,51 +1,23 @@
import { FormattedNumber, FormattedMessage } from 'react-intl';
-import ChatBubbleIcon from '@/material-icons/400-24px/chat_bubble.svg?react';
-import type { TimeSeriesMonth } from 'flavours/glitch/models/annual_report';
+import classNames from 'classnames';
+
+import styles from './index.module.scss';
export const NewPosts: React.FC<{
- data: TimeSeriesMonth[];
-}> = ({ data }) => {
- const posts = data.reduce((sum, item) => sum + item.statuses, 0);
-
+ count: number;
+}> = ({ count }) => {
return (
-
-
-
-
-
+
+
+
-
diff --git a/app/javascript/flavours/glitch/features/annual_report/share_button.tsx b/app/javascript/flavours/glitch/features/annual_report/share_button.tsx
new file mode 100644
index 0000000000..497d15d1e8
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/annual_report/share_button.tsx
@@ -0,0 +1,41 @@
+import { useCallback } from 'react';
+import type { FC } from 'react';
+
+import { useIntl } from 'react-intl';
+
+import { resetCompose, focusCompose } from '@/flavours/glitch/actions/compose';
+import { closeModal } from '@/flavours/glitch/actions/modal';
+import { Button } from '@/flavours/glitch/components/button';
+import type { AnnualReport as AnnualReportData } from '@/flavours/glitch/models/annual_report';
+import { useAppDispatch } from '@/flavours/glitch/store';
+
+import { shareMessage } from '.';
+import { archetypeNames } from './archetype';
+
+export const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
+ const handleShareClick = useCallback(() => {
+ // Generate the share message.
+ const archetypeName = intl.formatMessage(
+ archetypeNames[report.data.archetype],
+ );
+ const shareLines = [
+ intl.formatMessage(shareMessage, {
+ archetype: archetypeName,
+ }),
+ ];
+ // Share URL is only available for schema version 2.
+ if (report.schema_version === 2 && report.share_url) {
+ shareLines.push(report.share_url);
+ }
+ shareLines.push(`#Wrapstodon${report.year}`);
+
+ // Reset the composer and focus it with the share message, then close the modal.
+ dispatch(resetCompose());
+ dispatch(focusCompose(shareLines.join('\n\n')));
+ dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
+ }, [report, intl, dispatch]);
+
+ return
;
+};
diff --git a/app/javascript/flavours/glitch/features/annual_report/share.module.css b/app/javascript/flavours/glitch/features/annual_report/shared_page.module.css
similarity index 83%
rename from app/javascript/flavours/glitch/features/annual_report/share.module.css
rename to app/javascript/flavours/glitch/features/annual_report/shared_page.module.css
index e4d01ff3e3..99b713c05d 100644
--- a/app/javascript/flavours/glitch/features/annual_report/share.module.css
+++ b/app/javascript/flavours/glitch/features/annual_report/shared_page.module.css
@@ -1,6 +1,6 @@
.wrapper {
- max-width: 40rem;
- margin: 0 auto;
+ max-width: max-content;
+ margin: 40px auto;
}
.footer {
diff --git a/app/javascript/flavours/glitch/features/annual_report/share.tsx b/app/javascript/flavours/glitch/features/annual_report/shared_page.tsx
similarity index 74%
rename from app/javascript/flavours/glitch/features/annual_report/share.tsx
rename to app/javascript/flavours/glitch/features/annual_report/shared_page.tsx
index b8a8c0df1f..2d28740fc3 100644
--- a/app/javascript/flavours/glitch/features/annual_report/share.tsx
+++ b/app/javascript/flavours/glitch/features/annual_report/shared_page.tsx
@@ -3,12 +3,12 @@ import type { FC } from 'react';
import { IconLogo } from '@/flavours/glitch/components/logo';
import { AnnualReport } from './index';
-import classes from './share.module.css';
+import classes from './shared_page.module.css';
-export const WrapstodonShare: FC = () => {
+export const WrapstodonSharedPage: FC = () => {
return (
-
+