@antscorp/antsomi-ui 1.3.6-beta.77 → 1.3.6-beta.78

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.
@@ -3,6 +3,7 @@ import 'swiper/swiper-bundle.css';
3
3
  import type { MessageStructure } from '../types';
4
4
  type CarouselMessageProps = Pick<MessageStructure, 'body' | 'carousel'> & {
5
5
  messageIndex: number;
6
+ limitedTimeOffer: MessageStructure['limitedTimeOffer'];
6
7
  };
7
8
  declare const _default: React.MemoExoticComponent<(props: CarouselMessageProps) => import("react/jsx-runtime").JSX.Element>;
8
9
  export default _default;
@@ -17,13 +17,13 @@ import FooterMessage from '../FooterMessage';
17
17
  import { BASE } from '../../constants';
18
18
  // Utils
19
19
  import { getMediaSrc, getWhatsappComponentCls, isShowMedia } from '../utils';
20
+ import { globalToken } from '@antscorp/antsomi-ui/es/constants';
20
21
  const { MARGIN_MESSAGE, PADDING_MESSAGE } = BASE.OFFSET;
21
- const headerStyle = { padding: PADDING_MESSAGE, paddingBottom: 0 };
22
22
  const bodyStyle = { padding: PADDING_MESSAGE };
23
23
  const mediaStyle = { minWidth: BASE.LARGE.WIDTH };
24
24
  const cls = getWhatsappComponentCls();
25
25
  export default React.memo((props) => {
26
- const { carousel, body, messageIndex } = props;
26
+ const { carousel, limitedTimeOffer, body, messageIndex } = props;
27
27
  const isFirst = messageIndex === 0;
28
28
  // Refs
29
29
  const navigationPrevRef = useRef(null);
@@ -58,11 +58,18 @@ export default React.memo((props) => {
58
58
  const { header, body, buttons } = message;
59
59
  const isHeaderTxt = header?.format === 'TEXT';
60
60
  const isDoc = header?.format === 'DOCUMENT';
61
+ const isShowHeader = isHeaderTxt || isDoc || !!limitedTimeOffer?.text;
61
62
  return (_jsx(SwiperSlideItem, { virtualIndex: cardIdx, children: _jsx(SkeletonMessage, { size: "large", messagePadding: 0, styles: {
62
- header: headerStyle,
63
+ header: {
64
+ padding: PADDING_MESSAGE,
65
+ paddingBottom: limitedTimeOffer?.text ? PADDING_MESSAGE : 0,
66
+ borderBottom: limitedTimeOffer?.text
67
+ ? `1px solid ${globalToken?.bw2}`
68
+ : 'none',
69
+ },
63
70
  body: bodyStyle,
64
71
  media: mediaStyle,
65
- }, media: isShowMedia(header?.format) ? (_jsx(MediaMessage, { src: getMediaSrc(header), format: header?.format })) : null, header: isHeaderTxt || isDoc ? _jsx(HeaderMessage, { ...header }) : null, body: _jsx(BodyMessage, { ...body }), footer: _jsx(FooterMessage, { buttons: buttons }) }) }, cardIdx));
72
+ }, media: isShowMedia(header?.format) ? (_jsx(MediaMessage, { src: getMediaSrc(header), format: header?.format })) : null, header: isShowHeader ? (_jsx(HeaderMessage, { header: header, limitedTimeOffer: limitedTimeOffer })) : null, body: _jsx(BodyMessage, { ...body }), footer: _jsx(FooterMessage, { buttons: buttons }) }) }, cardIdx));
66
73
  }) })] }));
67
74
  }, []);
68
75
  return (_jsxs("div", { children: [_jsx("div", { style: {
@@ -1,4 +1,8 @@
1
1
  import React from 'react';
2
- import type { MessageHeader } from '../types';
3
- declare const _default: React.MemoExoticComponent<(props: MessageHeader) => import("react/jsx-runtime").JSX.Element | null>;
2
+ import type { MessageHeader, MessageStructure } from '../types';
3
+ type MessageHeaderProps = {
4
+ header: MessageHeader;
5
+ limitedTimeOffer: MessageStructure['limitedTimeOffer'];
6
+ };
7
+ declare const _default: React.MemoExoticComponent<(props: MessageHeaderProps) => import("react/jsx-runtime").JSX.Element | null>;
4
8
  export default _default;
@@ -5,19 +5,32 @@ import { memo } from 'react';
5
5
  import PDFImage from '@antscorp/antsomi-ui/es/assets/images/previews/pdf.png';
6
6
  // Components
7
7
  import { BaseParagraphMessage } from '../../styled';
8
- import { Flex } from 'antd';
8
+ import { Flex, Typography } from 'antd';
9
+ import { RedeemIcon } from '@antscorp/antsomi-ui/es/components/icons';
9
10
  // Utils
10
- import { getWhatsappComponentCls } from '../utils';
11
+ import { formatEndDateTime, getWhatsappComponentCls } from '../utils';
12
+ import { globalToken } from '@antscorp/antsomi-ui/es/constants';
11
13
  const cls = getWhatsappComponentCls();
12
14
  export default memo((props) => {
13
- const { format } = props;
15
+ const { header, limitedTimeOffer } = props;
16
+ const { format } = header;
17
+ if (limitedTimeOffer?.text) {
18
+ const { text, startDate, startTime, code } = limitedTimeOffer;
19
+ return (_jsxs(Flex, { gap: 10, align: "center", style: { height: 60 }, children: [_jsx(Flex, { justify: "center", align: "center", style: {
20
+ background: '#EEF5FC',
21
+ width: 40,
22
+ height: 40,
23
+ borderRadius: '50px',
24
+ flexShrink: 0,
25
+ }, children: _jsx(RedeemIcon, { size: 30, style: { fill: globalToken?.colorPrimary } }) }), _jsxs("div", { style: { maxWidth: 'calc(100% - 50px)' }, children: [_jsx(Typography.Text, { ellipsis: { tooltip: true }, style: { fontWeight: 800, display: 'block' }, children: text }), _jsx(Typography.Text, { ellipsis: { tooltip: true }, style: { fontSize: '11px', color: globalToken?.bw7, display: 'block' }, children: formatEndDateTime(startDate, startTime) }), _jsxs(Typography.Text, { ellipsis: { tooltip: true }, style: { fontSize: '11px', color: globalToken?.bw6, display: 'block' }, children: ["Code: ", code || 'BUY2GET1'] })] })] }));
26
+ }
14
27
  switch (format) {
15
28
  case 'TEXT': {
16
- const { text } = props;
29
+ const { text } = header;
17
30
  return (_jsx(BaseParagraphMessage, { className: `${cls('header')} base-paragraph-message`, style: { fontWeight: 800 }, ellipsis: { tooltip: true, rows: 3 }, children: text }));
18
31
  }
19
32
  case 'DOCUMENT': {
20
- const { documentName } = props;
33
+ const { documentName } = header;
21
34
  return (_jsxs(Flex, { align: "center", gap: 4, className: `${cls('header')}`, style: { minHeight: 48, padding: '0px 5px' }, children: [_jsx("img", { src: PDFImage, alt: "pdf icon", width: 20, height: 20, style: { objectFit: 'contain' } }), _jsx(BaseParagraphMessage, { className: "base-paragraph-message", ellipsis: { rows: 2, tooltip: true }, children: documentName || 'Document Name' })] }));
22
35
  }
23
36
  default:
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /* eslint-disable jsx-a11y/media-has-caption */
3
3
  // Libraries
4
- import { memo, useMemo, useState } from 'react';
4
+ import { memo, useEffect, useMemo, useState } from 'react';
5
5
  // Assets
6
6
  import AntLogo from '@antscorp/antsomi-ui/es/assets/images/logo/antsomi_logo.png';
7
7
  import MediaUnavailable from '@antscorp/antsomi-ui/es/assets/images/previews/media-unavailable.png';
@@ -29,6 +29,11 @@ export default memo((props) => {
29
29
  objectFit: objFit,
30
30
  };
31
31
  }, [format, src]);
32
+ useEffect(() => {
33
+ if (src && format === 'VIDEO') {
34
+ setIsVideoFails(false);
35
+ }
36
+ }, [src, format]);
32
37
  if (format === 'VIDEO') {
33
38
  if (!src || isVideoFails) {
34
39
  return (_jsx(Image, { src: isVideoFails ? MediaUnavailable : AntLogo, alt: "media message header", width: "100%", height: BASE.MEDIA.MAX_HEIGHT, style: {
@@ -39,7 +44,7 @@ export default memo((props) => {
39
44
  }
40
45
  return (_jsxs("video", { width: "100%", style: { height: BASE.MEDIA.MAX_HEIGHT }, controls: true, autoPlay: false, onError: () => {
41
46
  setIsVideoFails(true);
42
- }, children: [_jsx("source", { src: src, type: "video/mp4" }), "Your browser does not support the video tag."] }));
47
+ }, children: [_jsx("source", { src: src, type: "video/mp4" }), "Your browser does not support the video tag."] }, src));
43
48
  }
44
49
  return (_jsx(Image, { src: src || AntLogo, alt: "media message header", width: "100%", height: BASE.MEDIA.MAX_HEIGHT, style: {
45
50
  objectFit,
@@ -18,9 +18,9 @@ import CarouselMessage from './CarouselMessage';
18
18
  import { BASE } from '../constants';
19
19
  // Utils
20
20
  import { getMarginMessage, getMediaSrc, hasOnlyAttribute, isShowMedia } from './utils';
21
+ import { globalToken } from '@antscorp/antsomi-ui/es/constants';
21
22
  const { OFFSET } = BASE;
22
23
  const { PADDING_MESSAGE } = OFFSET;
23
- const headerStyle = { padding: PADDING_MESSAGE, paddingBottom: 0 };
24
24
  const bodyStyle = { padding: PADDING_MESSAGE };
25
25
  export default memo((props) => {
26
26
  const { branding, messages } = props;
@@ -28,13 +28,13 @@ export default memo((props) => {
28
28
  const content = messageList.map((message, messageIndex) => {
29
29
  const isFirstMessage = messageIndex === 0;
30
30
  const isLastMessage = messageIndex === messageList.length - 1;
31
- const { header, body, footer, buttons, carousel, type } = message;
31
+ const { header, body, footer, limitedTimeOffer, buttons, carousel, type } = message;
32
32
  if (!type || ['TEXT', 'MEDIA'].includes(type)) {
33
33
  const isTxt = type === 'TEXT';
34
34
  const isMedia = type === 'MEDIA';
35
35
  // Carousel Images
36
36
  if (carousel) {
37
- return (_jsx(CarouselMessage, { messageIndex: messageIndex, body: body, carousel: carousel }, messageIndex));
37
+ return (_jsx(CarouselMessage, { messageIndex: messageIndex, body: body, carousel: carousel, limitedTimeOffer: limitedTimeOffer }, messageIndex));
38
38
  }
39
39
  // Case text message
40
40
  if (isTxt || hasOnlyAttribute(message, 'body')) {
@@ -45,10 +45,15 @@ export default memo((props) => {
45
45
  if (isMedia) {
46
46
  const isHeaderTxt = header?.format === 'TEXT';
47
47
  const isDoc = header?.format === 'DOCUMENT';
48
+ const isShowHeader = isHeaderTxt || isDoc || !!limitedTimeOffer?.text;
48
49
  return (_jsx(SkeletonMessage, { size: "large", messagePadding: 0, messageMargin: getMarginMessage({ isFirst: isFirstMessage, isLast: isLastMessage }), styles: {
49
- header: headerStyle,
50
+ header: {
51
+ padding: PADDING_MESSAGE,
52
+ paddingBottom: limitedTimeOffer?.text ? PADDING_MESSAGE : 0,
53
+ borderBottom: limitedTimeOffer?.text ? `1px solid ${globalToken?.bw2}` : 'none',
54
+ },
50
55
  body: bodyStyle,
51
- }, media: isShowMedia(header?.format) ? (_jsx(MediaMessage, { src: getMediaSrc(header), format: header?.format })) : null, header: isHeaderTxt || isDoc ? _jsx(HeaderMessage, { ...header }) : null, body: _jsx(BodyMessage, { ...body }), footer: _jsx(FooterMessage, { footer: footer, buttons: buttons }) }, messageIndex));
56
+ }, media: isShowMedia(header?.format) ? (_jsx(MediaMessage, { src: getMediaSrc(header), format: header?.format })) : null, header: isShowHeader ? (_jsx(HeaderMessage, { header: header, limitedTimeOffer: limitedTimeOffer })) : null, body: _jsx(BodyMessage, { ...body }), footer: _jsx(FooterMessage, { footer: footer, buttons: buttons }) }, messageIndex));
52
57
  }
53
58
  }
54
59
  return (_jsx(BaseContainerMessage, { "$margin": getMarginMessage({ isFirst: isFirstMessage, isLast: isLastMessage }), children: _jsx(Empty, { description: translate(translations._NOTI_NOT_FOUND, 'Message not found!') }) }));
@@ -52,6 +52,9 @@ export interface MessageStructure {
52
52
  limitedTimeOffer?: {
53
53
  hasExpiration?: string;
54
54
  text: string;
55
+ startDate: number;
56
+ startTime: number;
57
+ code: string;
55
58
  };
56
59
  }
57
60
  export interface MobileProps {
@@ -9,3 +9,4 @@ export declare function hasOnlyAttribute(obj: Record<any, any>, attr: string): b
9
9
  export declare const isFixedMedia: (format?: MessageHeaderDefaultImage) => boolean | undefined;
10
10
  export declare const isShowMedia: (format?: MessageHeader['format']) => boolean | undefined;
11
11
  export declare const getMediaSrc: (header?: MessageHeader) => any;
12
+ export declare function formatEndDateTime(startDate: number, startTime: number): string;
@@ -33,3 +33,28 @@ export const getMediaSrc = (header) => {
33
33
  }
34
34
  }
35
35
  };
36
+ export function formatEndDateTime(startDate, startTime) {
37
+ // Convert timestamps to Date objects
38
+ const startDateObj = new Date(startDate);
39
+ const startTimeObj = new Date(startTime);
40
+ // Get current date
41
+ const currentDate = new Date();
42
+ // Function to pad single digits with zero
43
+ const padZero = (num) => num.toString().padStart(2, '0');
44
+ // Extract time from startTimeObj
45
+ const hours = padZero(startTimeObj.getHours());
46
+ const minutes = padZero(startTimeObj.getMinutes());
47
+ // Check if the start date is today
48
+ const isToday = startDateObj.getDate() === currentDate.getDate() &&
49
+ startDateObj.getMonth() === currentDate.getMonth() &&
50
+ startDateObj.getFullYear() === currentDate.getFullYear();
51
+ // If it's today, return "Ends today at HH:MM"
52
+ if (isToday) {
53
+ return `Ends today at ${hours}:${minutes}`;
54
+ }
55
+ // Calculate days difference
56
+ const timeDifference = startDateObj.getTime() - currentDate.getTime();
57
+ const daysDifference = Math.ceil(timeDifference / (1000 * 3600 * 24));
58
+ // Return "Ends in X days"
59
+ return `Ends in ${daysDifference} day${daysDifference !== 1 ? 's' : ''}`;
60
+ }
@@ -33,6 +33,25 @@ export const SAMPLE_PREVIEW = {
33
33
  },
34
34
  buttons: [{ type: 'URL', text: 'Grab the offer', url: 'https://test.com.vn' }],
35
35
  },
36
+ {
37
+ type: 'MEDIA',
38
+ header: {
39
+ format: 'IMAGE',
40
+ },
41
+ body: {
42
+ text: `Hi {{1}}, the item {{2}} you were wondering about is BACK IN STOCK! 🎉
43
+
44
+ Click the button to get it!
45
+ `,
46
+ },
47
+ limitedTimeOffer: {
48
+ text: 'Buy now!',
49
+ startDate: new Date().getTime(),
50
+ startTime: new Date().getTime(),
51
+ code: 'testing code',
52
+ },
53
+ buttons: [{ type: 'URL', text: 'Grab the offer', url: 'https://test.com.vn' }],
54
+ },
36
55
  {
37
56
  type: 'MEDIA',
38
57
  header: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antscorp/antsomi-ui",
3
- "version": "1.3.6-beta.77",
3
+ "version": "1.3.6-beta.78",
4
4
  "description": "An enterprise-class UI design language and React UI library.",
5
5
  "sideEffects": [
6
6
  "dist/*",