diff --git a/fixtures/webstudio-custom-template/app/__generated__/[sitemap.xml]._index.tsx b/fixtures/webstudio-custom-template/app/__generated__/[sitemap.xml]._index.tsx index 6f494dfccef1..43a322ffacf9 100644 --- a/fixtures/webstudio-custom-template/app/__generated__/[sitemap.xml]._index.tsx +++ b/fixtures/webstudio-custom-template/app/__generated__/[sitemap.xml]._index.tsx @@ -4,7 +4,7 @@ import { Fragment, useState } from "react"; import type { FontAsset, ImageAsset } from "@webstudio-is/sdk"; import { useResource, useVariableState } from "@webstudio-is/react-sdk/runtime"; -import { XmlNode } from "@webstudio-is/sdk-components-react"; +import { XmlNode, XmlTime } from "@webstudio-is/sdk-components-react"; export const siteName = "Fixture Site"; diff --git a/fixtures/webstudio-remix-vercel/.webstudio/data.json b/fixtures/webstudio-remix-vercel/.webstudio/data.json index 9239d458ba4b..6a8f0c422e66 100644 --- a/fixtures/webstudio-remix-vercel/.webstudio/data.json +++ b/fixtures/webstudio-remix-vercel/.webstudio/data.json @@ -1,10 +1,10 @@ { "build": { - "id": "00bc4703-0722-4929-9647-fbe0ae091b94", + "id": "0ff71ecc-db91-41d0-ba52-26d2fc6c196d", "projectId": "cddc1d44-af37-4cb6-a430-d300cf6f932d", - "version": 392, - "createdAt": "2024-12-02T14:50:20.265+00:00", - "updatedAt": "2024-12-02T14:50:20.265+00:00", + "version": 396, + "createdAt": "2024-12-05T12:47:15.161+00:00", + "updatedAt": "2024-12-05T12:47:15.161+00:00", "pages": { "meta": { "siteName": "KittyGuardedZone", @@ -2857,6 +2857,16 @@ "type": "string", "value": "title" } + ], + [ + "q2FTulKKH-VEhZpaVqYoj", + { + "id": "q2FTulKKH-VEhZpaVqYoj", + "instanceId": "NBRETFGlP8H6t_1ZLbJ5J", + "name": "datetime", + "type": "string", + "value": "1733402818245" + } ] ], "dataSources": [ @@ -4370,8 +4380,8 @@ "component": "XmlNode", "children": [ { - "type": "text", - "value": "2020-10-10" + "type": "id", + "value": "NBRETFGlP8H6t_1ZLbJ5J" } ] } @@ -4823,6 +4833,15 @@ "label": "Templates", "children": [] } + ], + [ + "NBRETFGlP8H6t_1ZLbJ5J", + { + "type": "instance", + "id": "NBRETFGlP8H6t_1ZLbJ5J", + "component": "XmlTime", + "children": [] + } ] ], "deployment": { diff --git a/fixtures/webstudio-remix-vercel/app/__generated__/$resources.sitemap.xml.ts b/fixtures/webstudio-remix-vercel/app/__generated__/$resources.sitemap.xml.ts index 46b231d82ce3..c6ad42aa9a89 100644 --- a/fixtures/webstudio-remix-vercel/app/__generated__/$resources.sitemap.xml.ts +++ b/fixtures/webstudio-remix-vercel/app/__generated__/$resources.sitemap.xml.ts @@ -1,26 +1,26 @@ export const sitemap = [ { path: "/", - lastModified: "2024-12-02", + lastModified: "2024-12-05", }, { path: "/_route_with_symbols_", - lastModified: "2024-12-02", + lastModified: "2024-12-05", }, { path: "/form", - lastModified: "2024-12-02", + lastModified: "2024-12-05", }, { path: "/heading-with-id", - lastModified: "2024-12-02", + lastModified: "2024-12-05", }, { path: "/resources", - lastModified: "2024-12-02", + lastModified: "2024-12-05", }, { path: "/nested/nested-page", - lastModified: "2024-12-02", + lastModified: "2024-12-05", }, ]; diff --git a/fixtures/webstudio-remix-vercel/app/__generated__/[sitemap.xml]._index.tsx b/fixtures/webstudio-remix-vercel/app/__generated__/[sitemap.xml]._index.tsx index 090bbbbc801a..4fc23f38f155 100644 --- a/fixtures/webstudio-remix-vercel/app/__generated__/[sitemap.xml]._index.tsx +++ b/fixtures/webstudio-remix-vercel/app/__generated__/[sitemap.xml]._index.tsx @@ -4,7 +4,7 @@ import { Fragment, useState } from "react"; import type { FontAsset, ImageAsset } from "@webstudio-is/sdk"; import { useResource, useVariableState } from "@webstudio-is/react-sdk/runtime"; -import { XmlNode } from "@webstudio-is/sdk-components-react"; +import { XmlNode, XmlTime } from "@webstudio-is/sdk-components-react"; export const siteName = "KittyGuardedZone"; @@ -66,7 +66,9 @@ const Page = ({ system: system }: { system: any }) => { {"custom-hand-made-location"} - {"2020-10-10"} + + + scope.getName(component, shortName) ) - .filter((scopedName) => scopedName !== "XmlNode") + .filter( + (scopedName) => + scopedName !== "XmlNode" && scopedName !== "XmlTime" + ) .map((scopedName) => scopedName === "Body" ? // Using prevents React from hoisting elements like , , and diff --git a/packages/sdk-components-react/src/__generated__/xml-time.props.ts b/packages/sdk-components-react/src/__generated__/xml-time.props.ts new file mode 100644 index 000000000000..0e9c420c72ef --- /dev/null +++ b/packages/sdk-components-react/src/__generated__/xml-time.props.ts @@ -0,0 +1,17 @@ +import type { PropMeta } from "@webstudio-is/react-sdk"; + +export const props: Record = { + dateStyle: { + required: false, + control: "radio", + type: "string", + defaultValue: "short", + options: ["long", "short"], + }, + datetime: { + required: false, + control: "text", + type: "string", + defaultValue: "dateTime attribute is not set", + }, +}; diff --git a/packages/sdk-components-react/src/components.ts b/packages/sdk-components-react/src/components.ts index ee62bb5bd1bf..9f814926e2b9 100644 --- a/packages/sdk-components-react/src/components.ts +++ b/packages/sdk-components-react/src/components.ts @@ -33,6 +33,7 @@ export { VimeoPreviewImage } from "./vimeo-preview-image"; export { VimeoPlayButton } from "./vimeo-play-button"; export { VimeoSpinner } from "./vimeo-spinner"; export { XmlNode } from "./xml-node"; +export { XmlTime } from "./xml-time"; export { Time } from "./time"; export { Select } from "./select"; export { Option } from "./option"; diff --git a/packages/sdk-components-react/src/metas.ts b/packages/sdk-components-react/src/metas.ts index c8f231ddcf8d..c62853851a7a 100644 --- a/packages/sdk-components-react/src/metas.ts +++ b/packages/sdk-components-react/src/metas.ts @@ -34,6 +34,7 @@ export { meta as VimeoPreviewImage } from "./vimeo-preview-image.ws"; export { meta as VimeoPlayButton } from "./vimeo-play-button.ws"; export { meta as VimeoSpinner } from "./vimeo-spinner.ws"; export { meta as XmlNode } from "./xml-node.ws"; +export { meta as XmlTime } from "./xml-time.ws"; export { meta as Time } from "./time.ws"; export { meta as Select } from "./select.ws"; export { meta as Option } from "./option.ws"; diff --git a/packages/sdk-components-react/src/props.ts b/packages/sdk-components-react/src/props.ts index d66a11435fb2..fcfd070a591e 100644 --- a/packages/sdk-components-react/src/props.ts +++ b/packages/sdk-components-react/src/props.ts @@ -33,6 +33,7 @@ export { propsMeta as VimeoPreviewImage } from "./vimeo-preview-image.ws"; export { propsMeta as VimeoPlayButton } from "./vimeo-play-button.ws"; export { propsMeta as VimeoSpinner } from "./vimeo-spinner.ws"; export { propsMeta as XmlNode } from "./xml-node.ws"; +export { propsMeta as XmlTime } from "./xml-time.ws"; export { propsMeta as Time } from "./time.ws"; export { propsMeta as Select } from "./select.ws"; export { propsMeta as Option } from "./option.ws"; diff --git a/packages/sdk-components-react/src/xml-time.tsx b/packages/sdk-components-react/src/xml-time.tsx new file mode 100644 index 000000000000..8583b0d705eb --- /dev/null +++ b/packages/sdk-components-react/src/xml-time.tsx @@ -0,0 +1,62 @@ +import { forwardRef, useContext, type ElementRef } from "react"; +import { ReactSdkContext } from "@webstudio-is/react-sdk/runtime"; + +const DEFAULT_DATE_STYLE = "short"; +const INITIAL_DATE_STRING = "dateTime attribute is not set"; +const INVALID_DATE_STRING = ""; + +type XmlTimeProps = { + dateStyle?: "long" | "short"; + datetime: string; +}; + +const parseDate = (datetimeString: string) => { + if (datetimeString === "") { + return; + } + let date = new Date(datetimeString); + + // Check if the date already in valid format, e.g. "2024" + if (Number.isNaN(date.getTime()) === false) { + return date; + } + + // If its a number, we assume it's a timestamp and we may need to normalize it + if (/^\d+$/.test(datetimeString)) { + let timestamp = Number(datetimeString); + // Normalize a 10-digit timestamp to 13-digit + if (datetimeString.length === 10) { + timestamp *= 1000; + } + date = new Date(timestamp); + } + + if (Number.isNaN(date.getTime()) === false) { + return date; + } +}; + +export const XmlTime = forwardRef, XmlTimeProps>( + ({ dateStyle = DEFAULT_DATE_STYLE, datetime = INITIAL_DATE_STRING }, ref) => { + const { renderer } = useContext(ReactSdkContext); + + const datetimeString = + datetime === null ? INVALID_DATE_STRING : datetime.toString(); + + const date = parseDate(datetimeString); + + let formattedDate = datetimeString; + if (date) { + formattedDate = date.toISOString(); + if (dateStyle === "short") { + formattedDate = formattedDate.split("T")[0]; + } + } + + if (renderer === undefined) { + return formattedDate; + } + + return ; + } +); diff --git a/packages/sdk-components-react/src/xml-time.ws.ts b/packages/sdk-components-react/src/xml-time.ws.ts new file mode 100644 index 000000000000..3e4dfe6da6d9 --- /dev/null +++ b/packages/sdk-components-react/src/xml-time.ws.ts @@ -0,0 +1,22 @@ +import { CalendarIcon } from "@webstudio-is/icons/svg"; + +import { + type WsComponentMeta, + type WsComponentPropsMeta, +} from "@webstudio-is/react-sdk"; + +import { props } from "./__generated__/xml-time.props"; + +export const meta: WsComponentMeta = { + category: "xml", + type: "container", + description: "Converts machine-readable date and time to ISO format.", + icon: CalendarIcon, + stylable: false, + order: 7, +}; + +export const propsMeta: WsComponentPropsMeta = { + props, + initialProps: ["datetime", "dateStyle"], +};