@redocly/theme 0.53.0-next.3 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,7 @@
1
- declare const FORMATS: {
2
- timeago: (date: Date, locale: string) => string;
3
- iso: (date: Date) => string;
4
- short: (date: Date, locale: string) => string;
5
- long: (date: Date, locale: string) => string;
6
- };
1
+ type Formats = 'timeago' | 'iso' | 'short' | 'long';
7
2
  export type LastUpdatedProps = {
8
3
  lastModified: Date;
9
- format?: keyof typeof FORMATS;
4
+ format?: Formats;
10
5
  locale?: string;
11
6
  className?: string;
12
7
  };
@@ -27,22 +27,22 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
27
27
  };
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
29
  exports.LastUpdated = LastUpdated;
30
- const React = __importStar(require("react"));
30
+ const react_1 = __importStar(require("react"));
31
31
  const styled_components_1 = __importDefault(require("styled-components"));
32
- const timeago_js_1 = require("timeago.js");
33
- const hooks_1 = require("../../core/hooks");
34
32
  const constants_1 = require("../../core/constants");
35
- const FORMATS = {
36
- timeago: (date, locale) => (0, timeago_js_1.format)(date, locale),
37
- iso: (date) => date.toISOString().split('T')[0],
38
- short: (date, locale) => date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' }),
39
- long: (date, locale) => date.toLocaleDateString(locale, { month: 'long', day: 'numeric', year: 'numeric' }),
40
- };
33
+ const hooks_1 = require("../../core/hooks");
41
34
  function LastUpdated(props) {
42
35
  const { markdown: { lastUpdatedBlock = {} } = {} } = (0, hooks_1.useThemeConfig)();
43
36
  const { useTranslate, useL10nConfig } = (0, hooks_1.useThemeHooks)();
37
+ const { format: timeagoFormat } = (0, hooks_1.useTimeAgo)();
44
38
  const { translate } = useTranslate();
45
39
  const { currentLocale } = useL10nConfig();
40
+ const formats = (0, react_1.useMemo)(() => ({
41
+ timeago: (date) => timeagoFormat(date),
42
+ iso: (date) => date.toISOString().split('T')[0],
43
+ short: (date, locale) => date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' }),
44
+ long: (date, locale) => date.toLocaleDateString(locale, { month: 'long', day: 'numeric', year: 'numeric' }),
45
+ }), [timeagoFormat]);
46
46
  if (lastUpdatedBlock === null || lastUpdatedBlock === void 0 ? void 0 : lastUpdatedBlock.hide) {
47
47
  return null;
48
48
  }
@@ -52,15 +52,15 @@ function LastUpdated(props) {
52
52
  lastUpdatedBlock.locale ||
53
53
  (currentLocale !== constants_1.DEFAULT_LOCALE_PLACEHOLDER ? currentLocale || 'en-US' : 'en-US');
54
54
  const isoDate = lastModified.toISOString().split('T')[0];
55
- const lastUpdatedString = FORMATS[format](lastModified, locale);
55
+ const lastUpdatedString = formats[format](lastModified, locale);
56
56
  const translationKey = format === 'timeago' ? 'page.lastUpdated.timeago' : 'page.lastUpdated.on';
57
57
  const text = format === 'timeago'
58
58
  ? translate(translationKey, 'Last updated') + ' '
59
59
  : translate(translationKey, 'Last updated on');
60
- return (React.createElement(LastUpdatedWrapper, { className: props.className, "data-component-name": "LastUpdated/LastUpdated", rawOnPrint: format === 'timeago', "data-print-datetime": isoDate, "data-translation-key": translationKey },
60
+ return (react_1.default.createElement(LastUpdatedWrapper, { className: props.className, "data-component-name": "LastUpdated/LastUpdated", rawOnPrint: format === 'timeago', "data-print-datetime": isoDate, "data-translation-key": translationKey },
61
61
  text,
62
62
  " ",
63
- React.createElement("time", { dateTime: isoDate }, lastUpdatedString)));
63
+ react_1.default.createElement("time", { dateTime: isoDate }, lastUpdatedString)));
64
64
  }
65
65
  const LastUpdatedWrapper = styled_components_1.default.div `
66
66
  color: var(--last-updated-text-color);
@@ -24,3 +24,4 @@ export * from '../../../core/hooks/use-product-picker';
24
24
  export * from '../../../core/hooks/search/use-search-dialog';
25
25
  export * from '../../../core/hooks/use-language-picker';
26
26
  export * from '../../../core/hooks/__mocks__/use-element-size';
27
+ export * from '../../../core/hooks/__mocks__/use-time-ago';
@@ -40,4 +40,5 @@ __exportStar(require("../../../core/hooks/use-product-picker"), exports);
40
40
  __exportStar(require("../../../core/hooks/search/use-search-dialog"), exports);
41
41
  __exportStar(require("../../../core/hooks/use-language-picker"), exports);
42
42
  __exportStar(require("../../../core/hooks/__mocks__/use-element-size"), exports);
43
+ __exportStar(require("../../../core/hooks/__mocks__/use-time-ago"), exports);
43
44
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ export declare const useTimeAgo: jest.Mock<{
2
+ format: () => "5 days ago";
3
+ }, [], any>;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useTimeAgo = void 0;
4
+ exports.useTimeAgo = jest.fn(() => ({
5
+ format: () => '5 days ago',
6
+ }));
7
+ //# sourceMappingURL=use-time-ago.js.map
@@ -34,3 +34,4 @@ export * from '../../core/hooks/code-walkthrough/use-code-walkthrough-controls';
34
34
  export * from '../../core/hooks/code-walkthrough/use-code-panel';
35
35
  export * from '../../core/hooks/code-walkthrough/use-renderable-files';
36
36
  export * from '../../core/hooks/use-element-size';
37
+ export * from '../../core/hooks/use-time-ago';
@@ -50,4 +50,5 @@ __exportStar(require("../../core/hooks/code-walkthrough/use-code-walkthrough-con
50
50
  __exportStar(require("../../core/hooks/code-walkthrough/use-code-panel"), exports);
51
51
  __exportStar(require("../../core/hooks/code-walkthrough/use-renderable-files"), exports);
52
52
  __exportStar(require("../../core/hooks/use-element-size"), exports);
53
+ __exportStar(require("../../core/hooks/use-time-ago"), exports);
53
54
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ export declare function useTimeAgo(): {
2
+ format: (date: Date) => string;
3
+ };
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useTimeAgo = useTimeAgo;
4
+ const react_1 = require("react");
5
+ const hooks_1 = require("../../core/hooks");
6
+ const timeUnits = ['second', 'minute', 'hour', 'day', 'week', 'month', 'year'];
7
+ const timeUnitConversions = [
8
+ 60, // 60 seconds in 1 min
9
+ 60, // 60 mins in 1 hour
10
+ 24, // 24 hours in 1 day
11
+ 7, // 7 days in 1 week
12
+ 365 / 7 / 12, // 4.345238095238096 weeks in 1 month
13
+ 12, // 12 months in 1 year
14
+ ];
15
+ function useTimeAgo() {
16
+ const { useTranslate } = (0, hooks_1.useThemeHooks)();
17
+ const { translate } = useTranslate();
18
+ const format = (0, react_1.useCallback)((date) => {
19
+ let timeUnitIndex = 0;
20
+ let elapsedTime = (Date.now() - date.getTime()) / 1000;
21
+ for (; elapsedTime >= timeUnitConversions[timeUnitIndex] &&
22
+ timeUnitIndex < timeUnitConversions.length; timeUnitIndex++) {
23
+ elapsedTime /= timeUnitConversions[timeUnitIndex];
24
+ }
25
+ elapsedTime = Math.floor(elapsedTime);
26
+ if (timeUnitIndex === 0)
27
+ return translate('time.justNow', 'just now');
28
+ let timeUnit = timeUnits[timeUnitIndex];
29
+ if (elapsedTime > 1)
30
+ timeUnit = `${timeUnit}s`;
31
+ const translationKey = `time.past.${timeUnit}`;
32
+ return translate(translationKey, {
33
+ defaultValue: `${elapsedTime} ${timeUnit} ago`,
34
+ count: elapsedTime,
35
+ replace: { value: elapsedTime },
36
+ });
37
+ }, [translate]);
38
+ return { format };
39
+ }
40
+ //# sourceMappingURL=use-time-ago.js.map
@@ -0,0 +1 @@
1
+ export type Plural<T extends string> = `${T}s`;
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=common.js.map
@@ -11,3 +11,4 @@ export * from '../../core/types/sidebar';
11
11
  export * from '../../core/types/filter';
12
12
  export * from '../../core/types/user-menu';
13
13
  export * from '../../core/types/user-claims';
14
+ export * from '../../core/types/common';
@@ -27,4 +27,5 @@ __exportStar(require("../../core/types/sidebar"), exports);
27
27
  __exportStar(require("../../core/types/filter"), exports);
28
28
  __exportStar(require("../../core/types/user-menu"), exports);
29
29
  __exportStar(require("../../core/types/user-claims"), exports);
30
+ __exportStar(require("../../core/types/common"), exports);
30
31
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,5 @@
1
1
  import type { TOptions } from 'i18next';
2
- export type TranslationKey = 'dev.newApp' | 'dev.newApp.text' | 'dev.sidebar.header' | 'dev.sidebar.footer.text' | 'dev.create.app.dialog.appName.placeholder' | 'dev.create.app.dialog.appName.error' | 'dev.create.app.dialog.selectAPIs' | 'dev.create.app.dialog.description' | 'dev.create.app.dialog.description.placeholder' | 'dev.create.app.dialog.create' | 'dev.create.app.dialog.cancel' | 'dev.main.tab.appKeys' | 'dev.main.tab.logs' | 'dev.app.description.title' | 'dev.edit.description.dialog.title' | 'dev.edit.description.dialog.save' | 'dev.edit.description.dialog.cancel' | 'dev.edit.apis.dialog.selectedAPIs' | 'dev.app.key.create' | 'dev.create.key.dialog.title' | 'dev.create.key.dialog.create' | 'dev.create.key.dialog.cancel' | 'dev.app.edit' | 'dev.app.delete' | 'dev.edit.app.dialog.title' | 'dev.edit.app.dialog.save' | 'dev.edit.app.dialog.cancel' | 'dev.delete.app.dialog.title' | 'dev.delete.app.dialog.confirmation' | 'dev.delete.app.dialog.delete' | 'dev.delete.app.dialog.cancel' | 'dev.app.key.roll' | 'dev.roll.key.dialog.title' | 'dev.roll.key.dialog.apiKey' | 'dev.roll.key.dialog.expires' | 'dev.roll.key.dialog.confirmation' | 'dev.roll.key.dialog.cancel' | 'dev.roll.key.dialog.roll' | 'dev.update.key.dialog.title' | 'dev.update.key.dialog.update' | 'dev.update.key.dialog.cancel' | 'dev.app.key.api.name' | 'dev.app.key.api.status' | 'dev.app.key.api.edit' | 'dev.edit.apis.dialog.title' | 'dev.edit.apis.dialog.apiKey' | 'dev.edit.apis.dialog.save' | 'dev.edit.apis.dialog.cancel' | 'dev.select.placeholder' | 'dev.app.overview.status.pending' | 'dev.app.overview.status.approved' | 'dev.app.overview.status.revoked' | 'dev.app.overview.status' | 'dev.app.overview.non-production' | 'dev.app.overview.production' | 'dev.app.overview.clientId' | 'dev.app.overview.apiKey' | 'dev.app.key.revoke' | 'dev.revoke.key.dialog.title' | 'dev.revoke.key.dialog.apiKey' | 'dev.revoke.key.dialog.expires' | 'dev.revoke.key.dialog.confirmation' | 'dev.revoke.key.dialog.revoke' | 'dev.revoke.key.dialog.cancel' | 'dev.app.overview.expires' | 'dev.app.overview.created' | 'dev.app.overview.visibilityToggle.hide' | 'dev.app.overview.visibilityToggle.show' | 'search.loading' | 'search.noResults.title' | 'search.keys.navigate' | 'search.keys.select' | 'search.keys.exit' | 'search.label' | 'search.cancel' | 'search.recent' | 'search.navbar.label' | 'search.suggested' | 'search.showMore' | 'search.filter.title' | 'search.filter.reset' | 'search.filter.field.reset' | 'search.ai.welcomeText' | 'search.ai.newConversation' | 'search.ai.backToSearch' | 'search.ai.placeholder' | 'search.ai.generatingResponse' | 'search.ai.followUpQuestion' | 'search.ai.suggestionsTitle' | 'search.ai.thinkingText' | 'search.ai.resourcesFound' | 'search.ai.resourcesFound.basedOn' | 'search.ai.resourcesFound.resources' | 'search.ai.button' | 'search.ai.label' | 'search.ai.disclaimer' | 'search.ai.error.description' | 'search.ai.error.description.forbidden' | 'search.ai.error.description.unauthorized' | 'search.ai.error.header' | 'search.ai.error.header.forbidden' | 'search.ai.error.header.unauthorized' | 'toc.header' | 'footer.copyrightText' | 'page.homeButton' | 'page.forbidden.title' | 'page.notFound.title' | 'page.notFound.description' | 'page.lastUpdated.timeago' | 'page.lastUpdated.on' | 'catalog.filters.placeholder' | 'catalog.filters.title' | 'catalog.filters.clearAll' | 'catalog.filters.select.addFilter' | 'catalog.filters.select.all' | 'catalog.filters.done' | 'sidebar.menu.backLabel' | 'sidebar.menu.backToLabel' | 'sidebar.actions.show' | 'sidebar.actions.hide' | 'sidebar.actions.changeLayout' | 'versionPicker.label' | 'versionPicker.unversioned' | 'codeSnippet.copy.buttonText' | 'codeSnippet.copy.tooltipText' | 'codeSnippet.copy.toasterText' | 'markdown.editPage.text' | 'feedback.settings.comment.submitText' | 'feedback.settings.comment.label' | 'feedback.settings.comment.send' | 'feedback.settings.comment.cancel' | 'feedback.settings.comment.satisfiedLabel' | 'feedback.settings.comment.neutralLabel' | 'feedback.settings.comment.dissatisfiedLabel' | 'feedback.settings.submitText' | 'feedback.settings.label' | 'feedback.settings.reasons.label' | 'feedback.settings.reasons.send' | 'feedback.settings.comment.likeLabel' | 'feedback.settings.comment.dislikeLabel' | 'feedback.sentiment.thumbUp' | 'feedback.sentiment.thumbDown' | 'feedback.settings.leftScaleLabel' | 'feedback.settings.rightScaleLabel' | 'feedback.settings.optionalEmail.placeholder' | 'feedback.settings.optionalEmail.label' | 'codeSnippet.report.buttonText' | 'codeSnippet.report.tooltipText' | 'codeSnippet.report.label' | 'userMenu.login' | 'userMenu.logout' | 'userMenu.devOnboardingLabel' | 'mobileMenu.mainMenu' | 'mobileMenu.previous' | 'mobileMenu.products' | 'page.nextButton' | 'page.previousButton' | 'openapi.download.description.title' | 'openapi.info.title' | 'openapi.info.contact.url' | 'openapi.info.contact.name' | 'openapi.info.license' | 'openapi.info.termsOfService' | 'openapi.info.metadata.title' | 'openapi.key' | 'openapi.value' | 'openapi.enum' | 'openapi.items' | 'openapi.default' | 'openapi.variable' | 'openapi.variables' | 'openapi.actions.show' | 'openapi.actions.hide' | 'openapi.actions.more' | 'openapi.languages.title' | 'openapi.servers.title' | 'openapi.operations' | 'openapi.webhooks' | 'openapi.description' | 'openapi.badges.deprecated' | 'openapi.badges.required' | 'openapi.badges.webhook' | 'openapi.request' | 'openapi.path' | 'openapi.query' | 'openapi.cookie' | 'openapi.header' | 'openapi.body' | 'openapi.responses' | 'openapi.response' | 'openapi.callbacks' | 'openapi.callbackRequest' | 'openapi.callbackResponse' | 'openapi.payload' | 'openapi.discriminator' | 'openapi.contentType' | 'openapi.tryIt' | 'openapi.loading' | 'openapi.example' | 'openapi.examples' | 'openapi.additionalProperties' | 'openapi.patternProperties' | 'openapi.required' | 'openapi.recursive' | 'openapi.complex' | 'openapi.deprecated' | 'openapi.hideExample' | 'openapi.showExample' | 'openapi.expandAll' | 'openapi.collapseAll' | 'openapi.noResponseExample' | 'openapi.noResponseContent' | 'openapi.noRequestPayload' | 'openapi.hidePattern' | 'openapi.showPattern' | 'openapi.authorizationUrl' | 'openapi.tokenUrl' | 'openapi.refreshUrl' | 'openapi.scopes' | 'openapi.security' | 'openapi.httpAuthorizationScheme' | 'openapi.bearerFormat' | 'openapi.parameterName' | 'openapi.flowType' | 'openapi.connectUrl' | 'openapi.requiredScopes' | 'openapi.unsupportedLanguage' | 'openapi.failedToGenerateCodeSample' | 'asyncapi.download.description.title' | 'asyncapi.info.title' | 'graphql.queries' | 'graphql.mutations' | 'graphql.subscriptions' | 'graphql.directives' | 'graphql.objects' | 'graphql.interfaces' | 'graphql.unions' | 'graphql.enums' | 'graphql.inputs' | 'graphql.scalars' | 'graphql.arguments.label' | 'graphql.arguments.show' | 'graphql.arguments.hide' | 'graphql.arguments.here' | 'graphql.returnTypes.label' | 'graphql.returnTypes.show' | 'graphql.returnTypes.hide' | 'graphql.possibleTypes' | 'graphql.defaultValue' | 'graphql.deprecationReason' | 'graphql.implementedInterfaces' | 'graphql.nonNull' | 'graphql.required' | 'graphql.deprecated' | 'graphql.variables' | 'graphql.querySample' | 'graphql.mutationSample' | 'graphql.subscriptionSample' | 'graphql.responseSample' | 'graphql.locations' | 'graphql.sample' | 'graphql.referenced' | 'codeWalkthrough.download' | 'codeWalkthrough.preview';
2
+ export type TranslationKey = 'dev.newApp' | 'dev.newApp.text' | 'dev.sidebar.header' | 'dev.sidebar.footer.text' | 'dev.create.app.dialog.appName.placeholder' | 'dev.create.app.dialog.appName.error' | 'dev.create.app.dialog.selectAPIs' | 'dev.create.app.dialog.description' | 'dev.create.app.dialog.description.placeholder' | 'dev.create.app.dialog.create' | 'dev.create.app.dialog.cancel' | 'dev.main.tab.appKeys' | 'dev.main.tab.logs' | 'dev.app.description.title' | 'dev.edit.description.dialog.title' | 'dev.edit.description.dialog.save' | 'dev.edit.description.dialog.cancel' | 'dev.edit.apis.dialog.selectedAPIs' | 'dev.app.key.create' | 'dev.create.key.dialog.title' | 'dev.create.key.dialog.create' | 'dev.create.key.dialog.cancel' | 'dev.app.edit' | 'dev.app.delete' | 'dev.edit.app.dialog.title' | 'dev.edit.app.dialog.save' | 'dev.edit.app.dialog.cancel' | 'dev.delete.app.dialog.title' | 'dev.delete.app.dialog.confirmation' | 'dev.delete.app.dialog.delete' | 'dev.delete.app.dialog.cancel' | 'dev.app.key.roll' | 'dev.roll.key.dialog.title' | 'dev.roll.key.dialog.apiKey' | 'dev.roll.key.dialog.expires' | 'dev.roll.key.dialog.confirmation' | 'dev.roll.key.dialog.cancel' | 'dev.roll.key.dialog.roll' | 'dev.update.key.dialog.title' | 'dev.update.key.dialog.update' | 'dev.update.key.dialog.cancel' | 'dev.app.key.api.name' | 'dev.app.key.api.status' | 'dev.app.key.api.edit' | 'dev.edit.apis.dialog.title' | 'dev.edit.apis.dialog.apiKey' | 'dev.edit.apis.dialog.save' | 'dev.edit.apis.dialog.cancel' | 'dev.select.placeholder' | 'dev.app.overview.status.pending' | 'dev.app.overview.status.approved' | 'dev.app.overview.status.revoked' | 'dev.app.overview.status' | 'dev.app.overview.non-production' | 'dev.app.overview.production' | 'dev.app.overview.clientId' | 'dev.app.overview.apiKey' | 'dev.app.key.revoke' | 'dev.revoke.key.dialog.title' | 'dev.revoke.key.dialog.apiKey' | 'dev.revoke.key.dialog.expires' | 'dev.revoke.key.dialog.confirmation' | 'dev.revoke.key.dialog.revoke' | 'dev.revoke.key.dialog.cancel' | 'dev.app.overview.expires' | 'dev.app.overview.created' | 'dev.app.overview.visibilityToggle.hide' | 'dev.app.overview.visibilityToggle.show' | 'search.loading' | 'search.noResults.title' | 'search.keys.navigate' | 'search.keys.select' | 'search.keys.exit' | 'search.label' | 'search.cancel' | 'search.recent' | 'search.navbar.label' | 'search.suggested' | 'search.showMore' | 'search.filter.title' | 'search.filter.reset' | 'search.filter.field.reset' | 'search.ai.welcomeText' | 'search.ai.newConversation' | 'search.ai.backToSearch' | 'search.ai.placeholder' | 'search.ai.generatingResponse' | 'search.ai.followUpQuestion' | 'search.ai.suggestionsTitle' | 'search.ai.thinkingText' | 'search.ai.resourcesFound' | 'search.ai.resourcesFound.basedOn' | 'search.ai.resourcesFound.resources' | 'search.ai.button' | 'search.ai.label' | 'search.ai.disclaimer' | 'search.ai.error.description' | 'search.ai.error.description.forbidden' | 'search.ai.error.description.unauthorized' | 'search.ai.error.header' | 'search.ai.error.header.forbidden' | 'search.ai.error.header.unauthorized' | 'toc.header' | 'footer.copyrightText' | 'page.homeButton' | 'page.forbidden.title' | 'page.notFound.title' | 'page.notFound.description' | 'page.lastUpdated.timeago' | 'page.lastUpdated.on' | 'catalog.filters.placeholder' | 'catalog.filters.title' | 'catalog.filters.clearAll' | 'catalog.filters.select.addFilter' | 'catalog.filters.select.all' | 'catalog.filters.done' | 'sidebar.menu.backLabel' | 'sidebar.menu.backToLabel' | 'sidebar.actions.show' | 'sidebar.actions.hide' | 'sidebar.actions.changeLayout' | 'versionPicker.label' | 'versionPicker.unversioned' | 'codeSnippet.copy.buttonText' | 'codeSnippet.copy.tooltipText' | 'codeSnippet.copy.toasterText' | 'markdown.editPage.text' | 'feedback.settings.comment.submitText' | 'feedback.settings.comment.label' | 'feedback.settings.comment.send' | 'feedback.settings.comment.cancel' | 'feedback.settings.comment.satisfiedLabel' | 'feedback.settings.comment.neutralLabel' | 'feedback.settings.comment.dissatisfiedLabel' | 'feedback.settings.submitText' | 'feedback.settings.label' | 'feedback.settings.reasons.label' | 'feedback.settings.reasons.send' | 'feedback.settings.comment.likeLabel' | 'feedback.settings.comment.dislikeLabel' | 'feedback.sentiment.thumbUp' | 'feedback.sentiment.thumbDown' | 'feedback.settings.leftScaleLabel' | 'feedback.settings.rightScaleLabel' | 'feedback.settings.optionalEmail.placeholder' | 'feedback.settings.optionalEmail.label' | 'codeSnippet.report.buttonText' | 'codeSnippet.report.tooltipText' | 'codeSnippet.report.label' | 'userMenu.login' | 'userMenu.logout' | 'userMenu.devOnboardingLabel' | 'mobileMenu.mainMenu' | 'mobileMenu.previous' | 'mobileMenu.products' | 'page.nextButton' | 'page.previousButton' | 'openapi.download.description.title' | 'openapi.info.title' | 'openapi.info.contact.url' | 'openapi.info.contact.name' | 'openapi.info.license' | 'openapi.info.termsOfService' | 'openapi.info.metadata.title' | 'openapi.key' | 'openapi.value' | 'openapi.enum' | 'openapi.items' | 'openapi.default' | 'openapi.variable' | 'openapi.variables' | 'openapi.actions.show' | 'openapi.actions.hide' | 'openapi.actions.more' | 'openapi.languages.title' | 'openapi.servers.title' | 'openapi.operations' | 'openapi.webhooks' | 'openapi.description' | 'openapi.badges.deprecated' | 'openapi.badges.required' | 'openapi.badges.webhook' | 'openapi.request' | 'openapi.path' | 'openapi.query' | 'openapi.cookie' | 'openapi.header' | 'openapi.body' | 'openapi.responses' | 'openapi.response' | 'openapi.callbacks' | 'openapi.callbackRequest' | 'openapi.callbackResponse' | 'openapi.payload' | 'openapi.discriminator' | 'openapi.contentType' | 'openapi.tryIt' | 'openapi.loading' | 'openapi.example' | 'openapi.examples' | 'openapi.additionalProperties' | 'openapi.patternProperties' | 'openapi.required' | 'openapi.recursive' | 'openapi.complex' | 'openapi.deprecated' | 'openapi.hideExample' | 'openapi.showExample' | 'openapi.expandAll' | 'openapi.collapseAll' | 'openapi.noResponseExample' | 'openapi.noResponseContent' | 'openapi.noRequestPayload' | 'openapi.hidePattern' | 'openapi.showPattern' | 'openapi.authorizationUrl' | 'openapi.tokenUrl' | 'openapi.refreshUrl' | 'openapi.scopes' | 'openapi.security' | 'openapi.httpAuthorizationScheme' | 'openapi.bearerFormat' | 'openapi.parameterName' | 'openapi.flowType' | 'openapi.connectUrl' | 'openapi.requiredScopes' | 'openapi.unsupportedLanguage' | 'openapi.failedToGenerateCodeSample' | 'asyncapi.download.description.title' | 'asyncapi.info.title' | 'graphql.queries' | 'graphql.mutations' | 'graphql.subscriptions' | 'graphql.directives' | 'graphql.objects' | 'graphql.interfaces' | 'graphql.unions' | 'graphql.enums' | 'graphql.inputs' | 'graphql.scalars' | 'graphql.arguments.label' | 'graphql.arguments.show' | 'graphql.arguments.hide' | 'graphql.arguments.here' | 'graphql.returnTypes.label' | 'graphql.returnTypes.show' | 'graphql.returnTypes.hide' | 'graphql.possibleTypes' | 'graphql.defaultValue' | 'graphql.deprecationReason' | 'graphql.implementedInterfaces' | 'graphql.nonNull' | 'graphql.required' | 'graphql.deprecated' | 'graphql.variables' | 'graphql.querySample' | 'graphql.mutationSample' | 'graphql.subscriptionSample' | 'graphql.responseSample' | 'graphql.locations' | 'graphql.sample' | 'graphql.referenced' | 'codeWalkthrough.download' | 'codeWalkthrough.preview' | 'time.justNow' | 'time.past.second' | 'time.past.seconds' | 'time.past.minute' | 'time.past.minutes' | 'time.past.hour' | 'time.past.hours' | 'time.past.day' | 'time.past.days' | 'time.past.week' | 'time.past.weeks' | 'time.past.month' | 'time.past.months' | 'time.past.year' | 'time.past.years';
3
3
  export type Locale = {
4
4
  code: string;
5
5
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/theme",
3
- "version": "0.53.0-next.3",
3
+ "version": "0.53.0",
4
4
  "description": "Shared UI components lib",
5
5
  "keywords": [
6
6
  "theme",
@@ -88,7 +88,6 @@
88
88
  "nprogress": "0.2.0",
89
89
  "react-calendar": "4.2.1",
90
90
  "react-date-picker": "10.0.3",
91
- "timeago.js": "4.0.2",
92
91
  "@redocly/config": "0.24.1"
93
92
  },
94
93
  "scripts": {
@@ -1,22 +1,14 @@
1
- import * as React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import styled from 'styled-components';
3
- import { format } from 'timeago.js';
4
3
 
5
- import { useThemeHooks, useThemeConfig } from '@redocly/theme/core/hooks';
6
4
  import { DEFAULT_LOCALE_PLACEHOLDER } from '@redocly/theme/core/constants';
5
+ import { useThemeHooks, useThemeConfig, useTimeAgo } from '@redocly/theme/core/hooks';
7
6
 
8
- const FORMATS = {
9
- timeago: (date: Date, locale: string) => format(date, locale),
10
- iso: (date: Date) => date.toISOString().split('T')[0],
11
- short: (date: Date, locale: string) =>
12
- date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' }),
13
- long: (date: Date, locale: string) =>
14
- date.toLocaleDateString(locale, { month: 'long', day: 'numeric', year: 'numeric' }),
15
- };
7
+ type Formats = 'timeago' | 'iso' | 'short' | 'long';
16
8
 
17
9
  export type LastUpdatedProps = {
18
10
  lastModified: Date;
19
- format?: keyof typeof FORMATS;
11
+ format?: Formats;
20
12
  locale?: string;
21
13
  className?: string;
22
14
  };
@@ -24,9 +16,22 @@ export type LastUpdatedProps = {
24
16
  export function LastUpdated(props: LastUpdatedProps): JSX.Element | null {
25
17
  const { markdown: { lastUpdatedBlock = {} } = {} } = useThemeConfig();
26
18
  const { useTranslate, useL10nConfig } = useThemeHooks();
19
+ const { format: timeagoFormat } = useTimeAgo();
27
20
  const { translate } = useTranslate();
28
21
  const { currentLocale } = useL10nConfig();
29
22
 
23
+ const formats: Record<Formats, (date: Date, locale: string) => string> = useMemo(
24
+ () => ({
25
+ timeago: (date: Date) => timeagoFormat(date),
26
+ iso: (date: Date) => date.toISOString().split('T')[0],
27
+ short: (date: Date, locale: string) =>
28
+ date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' }),
29
+ long: (date: Date, locale: string) =>
30
+ date.toLocaleDateString(locale, { month: 'long', day: 'numeric', year: 'numeric' }),
31
+ }),
32
+ [timeagoFormat],
33
+ );
34
+
30
35
  if (lastUpdatedBlock?.hide) {
31
36
  return null;
32
37
  }
@@ -40,7 +45,7 @@ export function LastUpdated(props: LastUpdatedProps): JSX.Element | null {
40
45
 
41
46
  const isoDate = lastModified.toISOString().split('T')[0];
42
47
 
43
- const lastUpdatedString = FORMATS[format as keyof typeof FORMATS](lastModified, locale);
48
+ const lastUpdatedString = formats[format](lastModified, locale);
44
49
  const translationKey = format === 'timeago' ? 'page.lastUpdated.timeago' : 'page.lastUpdated.on';
45
50
 
46
51
  const text =
@@ -24,3 +24,4 @@ export * from '@redocly/theme/core/hooks/use-product-picker';
24
24
  export * from '@redocly/theme/core/hooks/search/use-search-dialog';
25
25
  export * from '@redocly/theme/core/hooks/use-language-picker';
26
26
  export * from '@redocly/theme/core/hooks/__mocks__/use-element-size';
27
+ export * from '@redocly/theme/core/hooks/__mocks__/use-time-ago';
@@ -0,0 +1,3 @@
1
+ export const useTimeAgo = jest.fn(() => ({
2
+ format: () => '5 days ago',
3
+ }));
@@ -34,3 +34,4 @@ export * from '@redocly/theme/core/hooks/code-walkthrough/use-code-walkthrough-c
34
34
  export * from '@redocly/theme/core/hooks/code-walkthrough/use-code-panel';
35
35
  export * from '@redocly/theme/core/hooks/code-walkthrough/use-renderable-files';
36
36
  export * from '@redocly/theme/core/hooks/use-element-size';
37
+ export * from '@redocly/theme/core/hooks/use-time-ago';
@@ -0,0 +1,54 @@
1
+ import { useCallback } from 'react';
2
+
3
+ import type { Plural, TranslationKey } from '@redocly/theme/core/types';
4
+
5
+ import { useThemeHooks } from '@redocly/theme/core/hooks';
6
+
7
+ const timeUnits = ['second', 'minute', 'hour', 'day', 'week', 'month', 'year'] as const;
8
+ const timeUnitConversions = [
9
+ 60, // 60 seconds in 1 min
10
+ 60, // 60 mins in 1 hour
11
+ 24, // 24 hours in 1 day
12
+ 7, // 7 days in 1 week
13
+ 365 / 7 / 12, // 4.345238095238096 weeks in 1 month
14
+ 12, // 12 months in 1 year
15
+ ] as const;
16
+
17
+ type TimeUnit = (typeof timeUnits)[number];
18
+
19
+ export function useTimeAgo() {
20
+ const { useTranslate } = useThemeHooks();
21
+ const { translate } = useTranslate();
22
+
23
+ const format = useCallback(
24
+ (date: Date) => {
25
+ let timeUnitIndex = 0;
26
+ let elapsedTime = (Date.now() - date.getTime()) / 1000;
27
+ for (
28
+ ;
29
+ elapsedTime >= timeUnitConversions[timeUnitIndex] &&
30
+ timeUnitIndex < timeUnitConversions.length;
31
+ timeUnitIndex++
32
+ ) {
33
+ elapsedTime /= timeUnitConversions[timeUnitIndex];
34
+ }
35
+ elapsedTime = Math.floor(elapsedTime);
36
+
37
+ if (timeUnitIndex === 0) return translate('time.justNow', 'just now');
38
+
39
+ let timeUnit: TimeUnit | Plural<TimeUnit> = timeUnits[timeUnitIndex];
40
+ if (elapsedTime > 1) timeUnit = `${timeUnit}s`;
41
+
42
+ const translationKey: TranslationKey = `time.past.${timeUnit}`;
43
+
44
+ return translate(translationKey, {
45
+ defaultValue: `${elapsedTime} ${timeUnit} ago`,
46
+ count: elapsedTime,
47
+ replace: { value: elapsedTime },
48
+ });
49
+ },
50
+ [translate],
51
+ );
52
+
53
+ return { format };
54
+ }
@@ -0,0 +1 @@
1
+ export type Plural<T extends string> = `${T}s`;
@@ -11,3 +11,4 @@ export * from '@redocly/theme/core/types/sidebar';
11
11
  export * from '@redocly/theme/core/types/filter';
12
12
  export * from '@redocly/theme/core/types/user-menu';
13
13
  export * from '@redocly/theme/core/types/user-claims';
14
+ export * from '@redocly/theme/core/types/common';
@@ -264,7 +264,22 @@ export type TranslationKey =
264
264
  | 'graphql.sample'
265
265
  | 'graphql.referenced'
266
266
  | 'codeWalkthrough.download'
267
- | 'codeWalkthrough.preview';
267
+ | 'codeWalkthrough.preview'
268
+ | 'time.justNow'
269
+ | 'time.past.second'
270
+ | 'time.past.seconds'
271
+ | 'time.past.minute'
272
+ | 'time.past.minutes'
273
+ | 'time.past.hour'
274
+ | 'time.past.hours'
275
+ | 'time.past.day'
276
+ | 'time.past.days'
277
+ | 'time.past.week'
278
+ | 'time.past.weeks'
279
+ | 'time.past.month'
280
+ | 'time.past.months'
281
+ | 'time.past.year'
282
+ | 'time.past.years';
268
283
 
269
284
  export type Locale = { code: string; name: string };
270
285