refactor: replace date-fns with use-relative-time (#4738)

See https://github.com/nkzw-tech/use-relative-time

We use only single function from date-fns while the package is like
22MB. Here's small alternative for react based on intl.

Added jsx support to ours toasts inside of builder and RelativeTime
component to use it inside of callbacks.
This commit is contained in:
Bogdan Chadkin
2025-01-11 20:26:16 +07:00
committed by GitHub
parent b2521e9682
commit db95b55552
8 changed files with 71 additions and 48 deletions

View File

@ -1,4 +1,3 @@
import { formatDistance } from "date-fns/formatDistance";
import {
AutogrowTextArea,
Box,
@ -41,6 +40,7 @@ import { AiApiException, RateLimitException } from "./api-exceptions";
import { getSetting, setSetting } from "~/builder/shared/client-settings";
import { flushSync } from "react-dom";
import { $selectedPage } from "~/shared/awareness";
import { RelativeTime } from "~/builder/shared/relative-time";
type PartialButtonProps<T = ComponentPropsWithoutRef<typeof Button>> = {
[key in keyof T]?: T[key];
@ -117,13 +117,10 @@ export const AiCommandBar = () => {
} catch (error) {
if (error instanceof RateLimitException) {
toast.info(
`Temporary AI rate limit reached. Please wait ${formatDistance(
Date.now(),
new Date(error.meta.reset),
{
includeSeconds: true,
}
)} and try again.`
<>
Temporary AI rate limit reached. Please wait{" "}
<RelativeTime time={new Date(error.meta.reset)} /> and try again.
</>
);
return;
}
@ -219,13 +216,10 @@ export const AiCommandBar = () => {
if (error instanceof RateLimitException) {
toast.info(
`Temporary AI rate limit reached. Please wait ${formatDistance(
Date.now(),
new Date(error.meta.reset),
{
includeSeconds: true,
}
)} and try again.`
<>
Temporary AI rate limit reached. Please wait{" "}
<RelativeTime time={new Date(error.meta.reset)} /> and try again.
</>
);
return;
}

View File

@ -26,8 +26,8 @@ import {
useOptimistic,
useRef,
useState,
type ReactNode,
} from "react";
import { formatDistance } from "date-fns/formatDistance";
import { Entri } from "./entri";
import { nativeClient } from "~/shared/trpc/trpc-client";
import { useStore } from "@nanostores/react";
@ -36,6 +36,7 @@ import { extractCname } from "./cname";
import { useEffectEvent } from "~/shared/hook-utils/effect-event";
import DomainCheckbox from "./domain-checkbox";
import { CopyToClipboard } from "~/builder/shared/copy-to-clipboard";
import { RelativeTime } from "~/builder/shared/relative-time";
export type Domain = Project["domainsVirtual"][number];
type DomainStatus = Project["domainsVirtual"][number]["status"];
@ -77,13 +78,12 @@ export const getPublishStatusAndText = ({
? "Publish failed"
: "Publishing started";
const statusText = `${textStart} ${formatDistance(
new Date(createdAt),
new Date(),
{
addSuffix: true,
}
)}`;
const statusText = (
<>
{textStart}
<RelativeTime time={new Date(createdAt)} />
</>
);
return { statusText, status };
};
@ -95,7 +95,7 @@ const getStatusText = (props: {
const status = getStatus(props.projectDomain);
let isVerifiedActive = false;
let text = "Something went wrong";
let text: ReactNode = "Something went wrong";
switch (status) {
case "UNVERIFIED":

View File

@ -58,10 +58,10 @@ import { humanizeString } from "~/shared/string-utils";
import { trpcClient, nativeClient } from "~/shared/trpc/trpc-client";
import { isFeatureEnabled } from "@webstudio-is/feature-flags";
import type { Templates } from "@webstudio-is/sdk";
import { formatDistance } from "date-fns/formatDistance";
import DomainCheckbox, { domainToPublishName } from "./domain-checkbox";
import { CopyToClipboard } from "~/builder/shared/copy-to-clipboard";
import { $openProjectSettings } from "~/shared/nano-states/project-settings";
import { RelativeTime } from "~/builder/shared/relative-time";
type ChangeProjectDomainProps = {
project: Project;
@ -206,7 +206,9 @@ const Publish = ({
refresh: () => Promise<void>;
}) => {
const [publishError, setPublishError] = useState<undefined | string>();
const [publishError, setPublishError] = useState<
undefined | JSX.Element | string
>();
const [isPublishing, setIsPublishing] = useOptimistic(false);
const buttonRef = useRef<HTMLButtonElement>(null);
const [hasSelectedDomains, setHasSelectedDomains] = useState(false);
@ -276,12 +278,17 @@ const Publish = ({
if (publishResult.success === false) {
console.error(publishResult.error);
let error = publishResult.error;
let error: JSX.Element | string = publishResult.error;
if (publishResult.error === "NOT_IMPLEMENTED") {
error =
`Build data for publishing has been successfully created.\n\n` +
`Use Webstudio CLI to generate the code.\n\n` +
`https://docs.webstudio.is/university/self-hosting/cli`;
error = (
<>
Build data for publishing has been successfully created. Use{" "}
<Link href="https://docs.webstudio.is/university/self-hosting/cli">
Webstudio&nbsp;CLI
</Link>{" "}
to generate the code.
</>
);
}
setPublishError(error);
if (publishResult.error === "NOT_IMPLEMENTED") {
@ -391,13 +398,11 @@ const getStaticPublishStatusAndText = ({
? "Download failed"
: "Download started";
const statusText = `${textStart} ${formatDistance(
new Date(updatedAt),
new Date(),
{
addSuffix: true,
}
)}`;
const statusText = (
<>
{textStart} <RelativeTime time={new Date(updatedAt)} />
</>
);
return { statusText, status };
};

View File

@ -0,0 +1,5 @@
import useRelativeTime from "@nkzw/use-relative-time";
export const RelativeTime = ({ time }: { time: Date }) => {
return useRelativeTime(time.getTime());
};

View File

@ -5,9 +5,16 @@ import { uploadAssets } from "~/builder/shared/assets/use-assets";
const apiWindowNamespace = "__webstudio__$__builderApi";
type ToastHandler = (message: string) => void;
const _builderApi = {
isInitialized: () => true,
toast,
toast: {
info: toast.info as ToastHandler,
warn: toast.warn as ToastHandler,
error: toast.error as ToastHandler,
success: toast.success as ToastHandler,
},
uploadImages: async (srcs: string[]) => {
const urlToIds = await uploadAssets(
"image",

View File

@ -41,6 +41,7 @@
"@lezer/css": "^1.1.9",
"@lezer/highlight": "^1.2.1",
"@nanostores/react": "^0.8.0",
"@nkzw/use-relative-time": "^1.1.0",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-tooltip": "^1.1.6",
"@react-aria/interactions": "^3.22.5",
@ -81,7 +82,6 @@
"colord": "^2.9.3",
"cookie": "^1.0.1",
"css-tree": "^2.3.1",
"date-fns": "^3.6.0",
"debug": "^4.3.7",
"downshift": "^6.1.7",
"fast-deep-equal": "^3.1.3",

View File

@ -1,3 +1,4 @@
import type { JSX } from "react";
import * as ToastPrimitive from "@radix-ui/react-toast";
import hotToast, {
resolveValue,
@ -9,7 +10,6 @@ import { keyframes, rawTheme, styled, type CSS } from "../stitches.config";
import { Box } from "./box";
import { theme } from "../stitches.config";
import { Grid } from "./grid";
import { Text } from "./text";
import { Flex } from "./flex";
import { Tooltip } from "./tooltip";
@ -383,9 +383,12 @@ export const Toaster = () => {
type Options = Pick<ToastOptions, "duration" | "id" | "icon">;
export const toast = {
info: (value: string, options?: Options) => hotToast(value, options),
error: (value: string, options?: Options) => hotToast.error(value, options),
warn: (value: string, options?: Options) => hotToast.custom(value, options),
success: (value: string, options?: Options) =>
info: (value: JSX.Element | string, options?: Options) =>
hotToast(value, options),
error: (value: JSX.Element | string, options?: Options) =>
hotToast.error(value, options),
warn: (value: JSX.Element | string, options?: Options) =>
hotToast.custom(value, options),
success: (value: JSX.Element | string, options?: Options) =>
hotToast.success(value, options),
};

15
pnpm-lock.yaml generated
View File

@ -190,6 +190,9 @@ importers:
'@nanostores/react':
specifier: ^0.8.0
version: 0.8.0(nanostores@0.11.3)(react@18.3.0-canary-14898b6a9-20240318)
'@nkzw/use-relative-time':
specifier: ^1.1.0
version: 1.1.0(react@18.3.0-canary-14898b6a9-20240318)
'@radix-ui/react-select':
specifier: ^2.1.4
version: 2.1.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318)
@ -310,9 +313,6 @@ importers:
css-tree:
specifier: ^2.3.1
version: 2.3.1(patch_hash=epgcmebti7rfrc2ej4odb3t4jy)
date-fns:
specifier: ^3.6.0
version: 3.6.0
debug:
specifier: ^4.3.7
version: 4.3.7
@ -3483,6 +3483,11 @@ packages:
resolution: {integrity: sha512-q3L9i3HoNfz0SGpTIS4zTcKBbRkxzCRpd169eyiTuk3IwcPC3/85mzLHranlKo2b+HYT0gu37YxGB45aD8A3Tw==}
engines: {node: '>=18.0.0'}
'@nkzw/use-relative-time@1.1.0':
resolution: {integrity: sha512-ogCL62FvScpRpsZUuaN6Jt0xPGRv62atQUNGyMcX+nZs4H5Fs5K1iA3MbSmkJ1y0n/N0RIRLc3VAp8o46lq2CA==}
peerDependencies:
react: 18.3.0-canary-14898b6a9-20240318
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -9918,6 +9923,10 @@ snapshots:
'@netlify/node-cookies': 0.1.0
urlpattern-polyfill: 8.0.2
'@nkzw/use-relative-time@1.1.0(react@18.3.0-canary-14898b6a9-20240318)':
dependencies:
react: 18.3.0-canary-14898b6a9-20240318
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5