From 1811bb9184664585aaffc7a8ed4ec6a146643212 Mon Sep 17 00:00:00 2001 From: Hsu Zhong Jun <27919917+dcshzj@users.noreply.github.com> Date: Thu, 26 Oct 2023 15:09:48 +0800 Subject: [PATCH] feat(media): introduce new image preview card common component (#1620) * feat(utils): enhance time diff calculation function * feat(media): introduce new image preview card common component * style(media): update based on design feedback * style(media): switch to use outline instead of border based on design --- package-lock.json | 16 +- package.json | 1 + .../ContextMenu/ContextMenuButton.tsx | 2 +- .../ContextMenu/ContextMenuItem.tsx | 7 +- .../ImagePreviewCard.stories.tsx | 27 +++ .../ImagePreviewCard/ImagePreviewCard.tsx | 171 ++++++++++++++++++ src/components/ImagePreviewCard/index.ts | 1 + .../siteDashboardHooks/useGetLastUpdated.ts | 2 +- src/layouts/Sites.tsx | 2 +- src/theme/components/Checkbox.ts | 16 ++ src/theme/components/index.ts | 2 + src/utils/dateUtils.ts | 26 ++- 12 files changed, 255 insertions(+), 18 deletions(-) create mode 100644 src/components/ImagePreviewCard/ImagePreviewCard.stories.tsx create mode 100644 src/components/ImagePreviewCard/ImagePreviewCard.tsx create mode 100644 src/components/ImagePreviewCard/index.ts create mode 100644 src/theme/components/Checkbox.ts diff --git a/package-lock.json b/package-lock.json index ce2dd37e8..d3953733a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "dependencies": { "@braintree/sanitize-url": "^6.0.1", + "@chakra-ui/anatomy": "^2.2.1", "@chakra-ui/cli": "^2.4.1", "@chakra-ui/react": "2.7.1", "@chakra-ui/theme": "^3.1.2", @@ -3135,8 +3136,9 @@ } }, "node_modules/@chakra-ui/anatomy": { - "version": "2.2.0", - "license": "MIT" + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/anatomy/-/anatomy-2.2.1.tgz", + "integrity": "sha512-bbmyWTGwQo+aHYDMtLIj7k7hcWvwE7GFVDViLFArrrPhfUTDdQTNqhiDp1N7eh2HLyjNhc2MKXV8s2KTQqkmTg==" }, "node_modules/@chakra-ui/avatar": { "version": "2.2.11", @@ -4152,6 +4154,11 @@ "@chakra-ui/styled-system": ">=2.0.0" } }, + "node_modules/@chakra-ui/theme-tools/node_modules/@chakra-ui/anatomy": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/anatomy/-/anatomy-2.2.0.tgz", + "integrity": "sha512-cD8Ms5C8+dFda0LrORMdxiFhAZwOIY1BSlCadz6/mHUIgNdQy13AHPrXiq6qWdMslqVHq10k5zH7xMPLt6kjFg==" + }, "node_modules/@chakra-ui/theme-utils": { "version": "2.0.18", "license": "MIT", @@ -4190,6 +4197,11 @@ "@chakra-ui/styled-system": ">=2.0.0" } }, + "node_modules/@chakra-ui/theme/node_modules/@chakra-ui/anatomy": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/anatomy/-/anatomy-2.2.0.tgz", + "integrity": "sha512-cD8Ms5C8+dFda0LrORMdxiFhAZwOIY1BSlCadz6/mHUIgNdQy13AHPrXiq6qWdMslqVHq10k5zH7xMPLt6kjFg==" + }, "node_modules/@chakra-ui/toast": { "version": "6.1.4", "license": "MIT", diff --git a/package.json b/package.json index 6a46ba835..61732cd53 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ }, "dependencies": { "@braintree/sanitize-url": "^6.0.1", + "@chakra-ui/anatomy": "^2.2.1", "@chakra-ui/cli": "^2.4.1", "@chakra-ui/react": "2.7.1", "@chakra-ui/theme": "^3.1.2", diff --git a/src/components/ContextMenu/ContextMenuButton.tsx b/src/components/ContextMenu/ContextMenuButton.tsx index 2a66706d7..69b41acc9 100644 --- a/src/components/ContextMenu/ContextMenuButton.tsx +++ b/src/components/ContextMenu/ContextMenuButton.tsx @@ -11,7 +11,7 @@ export const ContextMenuButton = forwardRef( zIndex={2} position="absolute" bottom="1rem" - right="2rem" + right="1rem" as={IconButton} aria-label="Context Menu" icon={ diff --git a/src/components/ContextMenu/ContextMenuItem.tsx b/src/components/ContextMenu/ContextMenuItem.tsx index 649692636..8fcb486a0 100644 --- a/src/components/ContextMenu/ContextMenuItem.tsx +++ b/src/components/ContextMenu/ContextMenuItem.tsx @@ -6,8 +6,9 @@ export type ContextMenuItemProps = Omit // eslint-disable-next-line import/prefer-default-export export const ContextMenuItem = forwardRef( - ({ icon, ...props }: ContextMenuItemProps, ref): JSX.Element => { - const iconColour = useToken("colors", "icon.alt") + ({ icon, color, ...props }: ContextMenuItemProps, ref): JSX.Element => { + // This is a safe cast as we always pass in a string to the color prop + const iconColour = useToken("colors", (color as string) || "icon.alt") // NOTE: This will override the component props set on icon const innerIcon = icon && @@ -27,7 +28,7 @@ export const ContextMenuItem = forwardRef( }} borderRadius={0} ref={ref} - color="text.body" + color={color || "text.body"} _hover={{ textDecoration: "none", backgroundColor: "primary.100", diff --git a/src/components/ImagePreviewCard/ImagePreviewCard.stories.tsx b/src/components/ImagePreviewCard/ImagePreviewCard.stories.tsx new file mode 100644 index 000000000..e8641a72d --- /dev/null +++ b/src/components/ImagePreviewCard/ImagePreviewCard.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryFn } from "@storybook/react" + +import { ImagePreviewCard, ImagePreviewCardProps } from "./ImagePreviewCard" + +const imagePreviewCardMeta = { + title: "Components/Image Preview Card", + component: ImagePreviewCard, +} as Meta + +type ImagePreviewCardTemplateArgs = ImagePreviewCardProps + +const Template: StoryFn = ( + props: ImagePreviewCardTemplateArgs +) => { + return +} + +export const Default = Template.bind({}) +Default.args = { + name: "filename.png", + addedTime: 1693217477000, + mediaUrl: "https://placehold.co/600x400", + isMenuNeeded: true, + onCheck: (e) => console.log(e.target.checked), +} + +export default imagePreviewCardMeta diff --git a/src/components/ImagePreviewCard/ImagePreviewCard.tsx b/src/components/ImagePreviewCard/ImagePreviewCard.tsx new file mode 100644 index 000000000..3a1c4a0ea --- /dev/null +++ b/src/components/ImagePreviewCard/ImagePreviewCard.tsx @@ -0,0 +1,171 @@ +import { + Box, + Center, + chakra, + Grid, + GridItem, + Image, + Text, + useMultiStyleConfig, + VStack, +} from "@chakra-ui/react" +import { Checkbox } from "@opengovsg/design-system-react" +import { ChangeEvent, useState } from "react" +import { BiEditAlt, BiTrash } from "react-icons/bi" + +import { ContextMenu } from "components/ContextMenu" + +import { convertUtcToTimeDiff } from "utils/dateUtils" + +import { CARD_THEME_KEY } from "theme/components/Card" + +export interface ImagePreviewCardProps { + name: string + addedTime: number + mediaUrl: string + isMenuNeeded?: boolean + onCheck?: (event: ChangeEvent) => void +} + +// Note: This is written as a separate component as the current Card API is not +// flexible enough to support this use case. +export const ImagePreviewCard = ({ + name, + addedTime, + mediaUrl, + isMenuNeeded = true, + onCheck, +}: ImagePreviewCardProps): JSX.Element => { + const [isSelected, setIsSelected] = useState(false) + const styles = useMultiStyleConfig(CARD_THEME_KEY, {}) + const relativeTime = convertUtcToTimeDiff(addedTime) + + return ( + + {/* Checkbox overlay over image */} + { + setIsSelected(!isSelected) + if (onCheck) onCheck(e) + }} + /> + + + + + {/* White overlay over image */} + + +
+ +
+
+
+ + + + + {name} + + + Added {relativeTime} + + + + + +
+ {isMenuNeeded && ( + + + + } + // as={RouterLink} + // to={`${url}/editMediaSettings/${encodedName}`} + > + Rename image + + } + color="interaction.critical.default" + // as={RouterLink} + // to={`${url}/deleteMedia/${encodedName}`} + > + Delete image + + + + )} +
+ ) +} diff --git a/src/components/ImagePreviewCard/index.ts b/src/components/ImagePreviewCard/index.ts new file mode 100644 index 000000000..530a2d6eb --- /dev/null +++ b/src/components/ImagePreviewCard/index.ts @@ -0,0 +1 @@ +export * from "./ImagePreviewCard" diff --git a/src/hooks/siteDashboardHooks/useGetLastUpdated.ts b/src/hooks/siteDashboardHooks/useGetLastUpdated.ts index a8ae20165..51ea67e5b 100644 --- a/src/hooks/siteDashboardHooks/useGetLastUpdated.ts +++ b/src/hooks/siteDashboardHooks/useGetLastUpdated.ts @@ -12,7 +12,7 @@ import { useErrorToast } from "utils/toasts" const getRelativeLastUpdated = (siteName: string) => { return SiteDashboardService.getLastUpdated(siteName).then((res) => { - return convertUtcToTimeDiff(res.lastUpdated) + return `Updated ${convertUtcToTimeDiff(res.lastUpdated)}` }) } diff --git a/src/layouts/Sites.tsx b/src/layouts/Sites.tsx index c3ad84c37..edfedeb42 100644 --- a/src/layouts/Sites.tsx +++ b/src/layouts/Sites.tsx @@ -70,7 +70,7 @@ const SitesCard = ( {repoName} {lastUpdated && ( - {convertUtcToTimeDiff(lastUpdated)} + Updated {convertUtcToTimeDiff(lastUpdated)} )} diff --git a/src/theme/components/Checkbox.ts b/src/theme/components/Checkbox.ts new file mode 100644 index 000000000..c5ffc26ad --- /dev/null +++ b/src/theme/components/Checkbox.ts @@ -0,0 +1,16 @@ +import { checkboxAnatomy } from "@chakra-ui/anatomy" +import { ComponentMultiStyleConfig } from "@chakra-ui/theme" + +export const Checkbox: ComponentMultiStyleConfig = { + parts: checkboxAnatomy.keys, + variants: { + transparent: { + control: { + bg: "transparent", + borderColor: "interaction.main-subtle.default", + width: "1.25rem", + height: "1.25rem", + }, + }, + }, +} diff --git a/src/theme/components/index.ts b/src/theme/components/index.ts index 01593fe3b..e6cd9cc7d 100644 --- a/src/theme/components/index.ts +++ b/src/theme/components/index.ts @@ -1,5 +1,6 @@ import { Breadcrumb } from "./Breadcrumb" import { Card, CARD_THEME_KEY } from "./Card" +import { Checkbox } from "./Checkbox" import { DISPLAY_CARD_THEME_KEY, DisplayCard } from "./DisplayCard" import { Infobox } from "./Infobox" import { Rating } from "./Rating" @@ -8,6 +9,7 @@ import { Rating } from "./Rating" export const components = { [CARD_THEME_KEY]: Card, [DISPLAY_CARD_THEME_KEY]: DisplayCard, + Checkbox, Breadcrumb, Infobox, Rating, diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts index f01f47187..be856e9eb 100644 --- a/src/utils/dateUtils.ts +++ b/src/utils/dateUtils.ts @@ -4,18 +4,24 @@ import { formatDuration, intervalToDuration } from "date-fns" * Converts a date/time string retrieved from Github to human readable string representing time difference. * e.g. 2022-08-24T08:30:46Z to Updated today */ -export const convertUtcToTimeDiff = (lastUpdatedTime: string): string => { - const gapInUpdate = new Date().getTime() - new Date(lastUpdatedTime).getTime() +export const convertUtcToTimeDiff = (timestamp: string | number): string => { + const gapInUpdate = new Date().getTime() - new Date(timestamp).getTime() const numDaysAgo = Math.floor(gapInUpdate / (1000 * 60 * 60 * 24)) - // return a message for number of days ago repo was last updated - switch (numDaysAgo) { - case 0: - return "Updated today" - case 1: - return "Updated 1 day ago" - default: - return `Updated ${numDaysAgo} days ago` + + if (gapInUpdate < 1000 * 60) { + // Threshold for "just now" is 1 minute + return "just now" + } + if (numDaysAgo === 0) { + return "today" + } + if (numDaysAgo === 1) { + return "yesterday" + } + if (numDaysAgo >= 365) { + return "more than a year ago" } + return `${numDaysAgo} days ago` } /**