Files
owncast/web/components/ui/OfflineBanner/OfflineBanner.tsx
T
CopilotGitHubgabekcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>OwncastGabe Kangas
29b5100114 Create custom Translation component for better i18n handling (#4431)
* Initial plan

* Implement Translation component with Storybook stories

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Add Jest test for Translation component and demonstrate ?lang=de functionality

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Javascript formatting autofixes

* Create centralized type-safe localization system

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Add @testing-library/react to Translation component tests

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Fix code formatting errors with prettier and eslint

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Revert "Fix code formatting errors with prettier and eslint"

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* fix(js): eslinter errors

* fix(js): unused code warnings

* fix(js): fix additional warnings

* Update Emoji admin page to use new Translation component

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Organize localization keys by logical sections (Frontend, Admin, Common, Testing)

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Organize localization keys by TypeScript namespaces

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Javascript formatting autofixes

* feat(js): add support for default translated text

* chore: add default lang translations on commit

* fix(js): unused code warnings

* Update OfflineBanner component to use new Translation component

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* fix(js): fix localization extraction job

* chore(js): remove ts-node cli

* fix(css): fix css warning

* feat(js): add some additional translation strings via component

* chore: update extracted translations

* test: add tests for Translation component defaultText and fallback behavior

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* chore: update extracted translations

* Javascript formatting autofixes

* chore: call out new Translation component

* fix: linter warning

* chore: updated instructions

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: gabek <414923+gabek@users.noreply.github.com>
Co-authored-by: Owncast <owncast@owncast.online>
Co-authored-by: Gabe Kangas <gabek@real-ity.com>
2025-07-13 02:21:46 -07:00

121 lines
3.8 KiB
TypeScript

/* eslint-disable react/no-danger */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import { Divider } from 'antd';
import React, { FC } from 'react';
import { formatDistanceToNow } from 'date-fns';
import dynamic from 'next/dynamic';
import classNames from 'classnames';
import { useTranslation } from 'next-export-i18n';
import { Translation } from '../Translation/Translation';
import { Localization } from '../../../types/localization';
import styles from './OfflineBanner.module.scss';
// Lazy loaded components
const ClockCircleOutlined = dynamic(() => import('@ant-design/icons/ClockCircleOutlined'), {
ssr: false,
});
export type OfflineBannerProps = {
streamName: string;
customText?: string;
lastLive?: Date;
notificationsEnabled: boolean;
fediverseAccount?: string;
showsHeader?: boolean;
onNotifyClick?: () => void;
onFollowClick?: () => void;
className?: string;
};
export const OfflineBanner: FC<OfflineBannerProps> = ({
streamName,
customText,
lastLive,
notificationsEnabled,
fediverseAccount,
showsHeader = true,
onNotifyClick,
onFollowClick,
className,
}) => {
const { t } = useTranslation();
const handleSpanClick = (event: React.MouseEvent<HTMLSpanElement>) => {
const target = event.target as HTMLSpanElement;
if (target.classList.contains('notify-link')) {
onNotifyClick?.();
} else if (target.classList.contains('follow-link')) {
onFollowClick?.();
}
};
let text;
if (customText) {
text = customText;
} else if (!customText && notificationsEnabled && fediverseAccount) {
text = (
<Translation
translationKey={Localization.Frontend.offlineNotifyAndFediverse}
vars={{ streamer: streamName, fediverseAccount }}
defaultText="This stream is offline. You can <span class='notify-link'>be notified</span> the next time {{streamer}} goes live or <span class='follow-link'>follow</span> {{fediverseAccount}} on the Fediverse."
/>
);
} else if (!customText && notificationsEnabled) {
text = (
<Translation
translationKey={Localization.Frontend.offlineNotifyOnly}
vars={{ streamer: streamName }}
defaultText="This stream is offline. <span class='notify-link'>Be notified</span> the next time {{streamer}} goes live."
/>
);
} else if (!customText && fediverseAccount) {
text = (
<Translation
translationKey={Localization.Frontend.offlineFediverseOnly}
vars={{ fediverseAccount, streamer: streamName }}
defaultText="This stream is offline. <span class='follow-link'>Follow</span> {{fediverseAccount}} on the Fediverse to see the next time {{streamer}} goes live."
/>
);
} else {
text = (
<Translation
translationKey={Localization.Frontend.offlineBasic}
defaultText="This stream is offline. Check back soon!"
/>
);
}
return (
<div id="offline-banner" className={classNames(styles.outerContainer, className)}>
<div className={styles.innerContainer}>
{showsHeader && (
<>
<div className={styles.header}>{streamName}</div>
<Divider className={styles.separator} />
</>
)}
{customText ? (
<div className={styles.bodyText} dangerouslySetInnerHTML={{ __html: text }} />
) : (
<div
className={styles.bodyText}
onClick={handleSpanClick}
role="presentation"
style={{ cursor: 'pointer' }}
>
{text}
</div>
)}
{lastLive && (
<div className={styles.lastLiveDate}>
<ClockCircleOutlined className={styles.clockIcon} />
{`${t('Last live ago', { timeAgo: formatDistanceToNow(new Date(lastLive)) })}`}
</div>
)}
</div>
</div>
);
};