From 6a3d083d6629ac23ed891df522d91a8c1b3cf4c8 Mon Sep 17 00:00:00 2001 From: mbasadi Date: Fri, 9 Aug 2024 13:18:07 -0400 Subject: [PATCH 01/12] prettier --- .prettierrc | 6 ++ lib/api.ts | 10 +- lib/components/Notifications/Inbox.tsx | 28 +++--- lib/components/Notifications/InboxHeader.tsx | 14 +-- lib/components/Notifications/Notification.tsx | 48 +++++----- .../Notifications/NotificationCounter.tsx | 10 +- .../Notifications/NotificationFeed.tsx | 26 ++--- .../Notifications/NotificationLauncher.tsx | 52 +++++----- .../Notifications/NotificationPopup.tsx | 44 ++++----- lib/components/Notifications/UnreadBadge.tsx | 48 +++++----- lib/components/Notifications/index.tsx | 8 +- .../NotificationPreferencesPopup.tsx | 6 +- .../Preferences/PreferenceGroup.tsx | 48 +++++----- lib/components/Preferences/Preferences.tsx | 28 +++--- lib/components/Preferences/index.tsx | 4 +- lib/components/Provider/index.tsx | 96 +++++++++---------- lib/main.ts | 10 +- package-lock.json | 16 ++++ package.json | 3 + src/App.tsx | 12 +-- src/main.tsx | 8 +- 21 files changed, 275 insertions(+), 250 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..aa458dd --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "trailingComma": "none", + "singleQuote": true, + "printWidth": 80 +} diff --git a/lib/api.ts b/lib/api.ts index 8e53ab8..4d88e63 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -1,6 +1,6 @@ export const api = async ( endpoint: string, - method: "GET" | "POST" | "PATCH", + method: 'GET' | 'POST' | 'PATCH', resource: string, clientId: string, userId: string, @@ -8,16 +8,16 @@ export const api = async ( data?: any ): Promise => { const token = hashedUserId - ? btoa(clientId + ":" + userId + ":" + hashedUserId) - : btoa(clientId + ":" + userId); + ? btoa(clientId + ':' + userId + ':' + hashedUserId) + : btoa(clientId + ':' + userId); const res = await fetch( `${endpoint}/${clientId}/users/${encodeURIComponent(userId)}/${resource}`, { method, body: JSON.stringify(data), headers: { - Authorization: `Basic ${token}`, - }, + Authorization: `Basic ${token}` + } } ); diff --git a/lib/components/Notifications/Inbox.tsx b/lib/components/Notifications/Inbox.tsx index ab72b01..3ca6261 100644 --- a/lib/components/Notifications/Inbox.tsx +++ b/lib/components/Notifications/Inbox.tsx @@ -1,23 +1,23 @@ -import { Empty, List } from "antd"; -import { InboxHeader } from "./InboxHeader"; -import VirtualList from "rc-virtual-list"; -import { ImageShape, Notification } from "./Notification"; -import { NotificationAPIContext } from "../Provider"; -import { useContext } from "react"; -import { Filter, NotificationPopupProps } from "./NotificationPopup"; +import { Empty, List } from 'antd'; +import { InboxHeader } from './InboxHeader'; +import VirtualList from 'rc-virtual-list'; +import { ImageShape, Notification } from './Notification'; +import { NotificationAPIContext } from '../Provider'; +import { useContext } from 'react'; +import { Filter, NotificationPopupProps } from './NotificationPopup'; export enum Pagination { - INFINITE_SCROLL = "infinite_scroll", - PAGINATED = "paginated", + INFINITE_SCROLL = 'infinite_scroll', + PAGINATED = 'paginated' } type InboxProps = { pagination: keyof typeof Pagination; maxHeight: number; - filter: NotificationPopupProps["filter"]; + filter: NotificationPopupProps['filter']; imageShape: keyof typeof ImageShape; pageSize: number; - pagePosition: NotificationPopupProps["pagePosition"]; + pagePosition: NotificationPopupProps['pagePosition']; }; export const Inbox: React.FC = (props) => { @@ -41,7 +41,7 @@ export const Inbox: React.FC = (props) => { return (
- {props.pagination === "INFINITE_SCROLL" ? ( + {props.pagination === 'INFINITE_SCROLL' ? ( } dataSource={filterFunction(context.notifications)} @@ -98,7 +98,7 @@ export const Inbox: React.FC = (props) => { )} pagination={{ pageSize: props.pageSize, - align: "center", + align: 'center', position: props.pagePosition, showSizeChanger: false, simple: true, @@ -108,7 +108,7 @@ export const Inbox: React.FC = (props) => { ) { context.loadNotifications(); } - }, + } }} > {filterFunction(context.notifications).length === 0 && ( diff --git a/lib/components/Notifications/InboxHeader.tsx b/lib/components/Notifications/InboxHeader.tsx index 2e7be51..11d2d30 100644 --- a/lib/components/Notifications/InboxHeader.tsx +++ b/lib/components/Notifications/InboxHeader.tsx @@ -1,15 +1,15 @@ -import { CheckOutlined, SettingOutlined } from "@ant-design/icons"; -import { Button, Popover, Typography } from "antd"; +import { CheckOutlined, SettingOutlined } from '@ant-design/icons'; +import { Button, Popover, Typography } from 'antd'; export const InboxHeader = (props: { - markAsArchived: (ids: string[] | "ALL") => void; + markAsArchived: (ids: string[] | 'ALL') => void; }) => { return (
Notifications @@ -21,7 +21,7 @@ export const InboxHeader = (props: { size="small" type="text" onClick={() => { - props.markAsArchived("ALL"); + props.markAsArchived('ALL'); }} /> diff --git a/lib/components/Notifications/Notification.tsx b/lib/components/Notifications/Notification.tsx index f3e0da3..5c22153 100644 --- a/lib/components/Notifications/Notification.tsx +++ b/lib/components/Notifications/Notification.tsx @@ -1,16 +1,16 @@ -import { CheckOutlined } from "@ant-design/icons"; -import { Avatar, Badge, Button, Typography } from "antd"; -import { styled } from "styled-components"; -import TimeAgo from "javascript-time-ago"; -import en from "javascript-time-ago/locale/en"; -import ReactTimeAgo from "react-time-ago"; +import { CheckOutlined } from '@ant-design/icons'; +import { Avatar, Badge, Button, Typography } from 'antd'; +import { styled } from 'styled-components'; +import TimeAgo from 'javascript-time-ago'; +import en from 'javascript-time-ago/locale/en'; +import ReactTimeAgo from 'react-time-ago'; TimeAgo.addDefaultLocale(en); TimeAgo.addLocale(en); export enum ImageShape { - square = "square", - circle = "circle", + square = 'square', + circle = 'circle' } const NotificationDiv = styled.div<{ @@ -18,7 +18,7 @@ const NotificationDiv = styled.div<{ $seen: boolean; $archived: boolean; }>` - cursor: ${(props) => (props.$redirect ? "pointer" : "default")}; + cursor: ${(props) => (props.$redirect ? 'pointer' : 'default')}; &:hover { background: #eee !important; @@ -30,13 +30,13 @@ const NotificationDiv = styled.div<{ } &:hover .notification-archive-button { - visibility: ${(props) => (props.$archived ? "hidden" : "visible")}; + visibility: ${(props) => (props.$archived ? 'hidden' : 'visible')}; } `; export const Notification = (props: { notification: any; - markAsArchived: (ids: string[] | "ALL") => void; + markAsArchived: (ids: string[] | 'ALL') => void; markAsClicked: (id: string) => void; imageShape: keyof typeof ImageShape; }) => { @@ -52,12 +52,12 @@ export const Notification = (props: { } }} style={{ - padding: "16px 6px 16px 0", - background: "#fff", - position: "relative", - display: "flex", - alignItems: "center", - width: "100%", + padding: '16px 6px 16px 0', + background: '#fff', + position: 'relative', + display: 'flex', + alignItems: 'center', + width: '100%' }} >
@@ -66,7 +66,7 @@ export const Notification = (props: { size="large" style={{ marginRight: 8, - marginLeft: 12, + marginLeft: 12 }} shape={props.imageShape} /> @@ -74,7 +74,7 @@ export const Notification = (props: {
@@ -92,11 +92,11 @@ export const Notification = (props: {
diff --git a/lib/components/Notifications/NotificationCounter.tsx b/lib/components/Notifications/NotificationCounter.tsx index 2a5aa41..195d133 100644 --- a/lib/components/Notifications/NotificationCounter.tsx +++ b/lib/components/Notifications/NotificationCounter.tsx @@ -1,13 +1,13 @@ -import { PropsWithChildren } from "react"; -import { UnreadBadge, UnreadBadgeProps } from "./UnreadBadge"; +import { PropsWithChildren } from 'react'; +import { UnreadBadge, UnreadBadgeProps } from './UnreadBadge'; -export type NotificationCounterProps = Omit & { - counting?: UnreadBadgeProps["count"]; +export type NotificationCounterProps = Omit & { + counting?: UnreadBadgeProps['count']; }; export const NotificationCounter: React.FC< PropsWithChildren > = (props) => { - const counting = props.counting || "COUNT_UNOPENED_NOTIFICATIONS"; + const counting = props.counting || 'COUNT_UNOPENED_NOTIFICATIONS'; return ; }; diff --git a/lib/components/Notifications/NotificationFeed.tsx b/lib/components/Notifications/NotificationFeed.tsx index b6f8dd2..e1eb163 100644 --- a/lib/components/Notifications/NotificationFeed.tsx +++ b/lib/components/Notifications/NotificationFeed.tsx @@ -1,14 +1,14 @@ -import { useContext, useEffect } from "react"; -import { Inbox, Pagination } from "./Inbox"; -import { ImageShape } from "./Notification"; -import { NotificationAPIContext } from "../Provider"; -import { Filter } from "./NotificationPopup"; +import { useContext, useEffect } from 'react'; +import { Inbox, Pagination } from './Inbox'; +import { ImageShape } from './Notification'; +import { NotificationAPIContext } from '../Provider'; +import { Filter } from './NotificationPopup'; export type NotificationFeedProps = { imageShape?: keyof typeof ImageShape; pagination?: keyof typeof Pagination; pageSize?: number; - pagePosition?: "top" | "bottom"; + pagePosition?: 'top' | 'bottom'; infiniteScrollHeight?: number; style?: React.CSSProperties; filter?: keyof typeof Filter | ((n: any) => boolean); @@ -16,15 +16,15 @@ export type NotificationFeedProps = { export const NotificationFeed: React.FC = (props) => { const config: Required = { - imageShape: props.imageShape || "circle", - pagination: props.pagination || "INFINITE_SCROLL", + imageShape: props.imageShape || 'circle', + pagination: props.pagination || 'INFINITE_SCROLL', pageSize: props.pageSize || 10, - pagePosition: props.pagePosition || "top", + pagePosition: props.pagePosition || 'top', style: props.style || {}, filter: props.filter || Filter.ALL, infiniteScrollHeight: props.infiniteScrollHeight ? props.infiniteScrollHeight - : window.innerHeight * 0.75, + : window.innerHeight * 0.75 }; const context = useContext(NotificationAPIContext); @@ -46,9 +46,9 @@ export const NotificationFeed: React.FC = (props) => {
= ( popupHeight: props.popupHeight || 600, buttonIconSize: props.buttonIconSize || (props.buttonWidth ? props.buttonWidth / 2 : 20), - imageShape: props.imageShape || "circle", - pagination: props.pagination || "INFINITE_SCROLL", + imageShape: props.imageShape || 'circle', + pagination: props.pagination || 'INFINITE_SCROLL', pageSize: props.pageSize || 10, - pagePosition: props.pagePosition || "top", + pagePosition: props.pagePosition || 'top', style: { zIndex: 999, - ...props.style, + ...props.style }, unreadBadgeProps: props.unreadBadgeProps ?? {}, offsetX: props.offsetX || 16, offsetY: props.offsetY || 16, - position: props.position || "BOTTOM_RIGHT", - count: props.count || "COUNT_UNOPENED_NOTIFICATIONS", - filter: props.filter || "ALL", + position: props.position || 'BOTTOM_RIGHT', + count: props.count || 'COUNT_UNOPENED_NOTIFICATIONS', + filter: props.filter || 'ALL' }; const context = useContext(NotificationAPIContext); @@ -54,10 +54,10 @@ export const NotificationLauncher: React.FC = ( return (
= ( } arrow={false} overlayStyle={{ - padding: "0 16px", - minWidth: config.popupWidth, + padding: '0 16px', + minWidth: config.popupWidth }} onOpenChange={(visible) => { if (visible) { @@ -86,14 +86,14 @@ export const NotificationLauncher: React.FC = ( >
@@ -101,13 +101,13 @@ export const NotificationLauncher: React.FC = ( icon={ } style={{ width: config.buttonWidth, - height: config.buttonHeight, + height: config.buttonHeight }} shape="circle" type="default" diff --git a/lib/components/Notifications/NotificationPopup.tsx b/lib/components/Notifications/NotificationPopup.tsx index c2c8fd1..106fafd 100644 --- a/lib/components/Notifications/NotificationPopup.tsx +++ b/lib/components/Notifications/NotificationPopup.tsx @@ -1,14 +1,14 @@ -import { Button, Popover } from "antd"; -import { Inbox, Pagination } from "./Inbox"; -import { BellOutlined } from "@ant-design/icons"; -import { UnreadBadge, UnreadBadgeProps } from "./UnreadBadge"; -import { ImageShape } from "./Notification"; -import { NotificationAPIContext } from "../Provider"; -import { useContext } from "react"; +import { Button, Popover } from 'antd'; +import { Inbox, Pagination } from './Inbox'; +import { BellOutlined } from '@ant-design/icons'; +import { UnreadBadge, UnreadBadgeProps } from './UnreadBadge'; +import { ImageShape } from './Notification'; +import { NotificationAPIContext } from '../Provider'; +import { useContext } from 'react'; export enum Filter { - ALL = "ALL", - UNARCHIVED = "UNARCHIVED", + ALL = 'ALL', + UNARCHIVED = 'UNARCHIVED' } export type NotificationPopupProps = { @@ -20,10 +20,10 @@ export type NotificationPopupProps = { imageShape?: keyof typeof ImageShape; pagination?: keyof typeof Pagination; pageSize?: number; - pagePosition?: "top" | "bottom"; + pagePosition?: 'top' | 'bottom'; style?: React.CSSProperties; unreadBadgeProps?: UnreadBadgeProps; - count?: UnreadBadgeProps["count"]; + count?: UnreadBadgeProps['count']; filter?: keyof typeof Filter | ((n: any) => boolean); }; @@ -35,14 +35,14 @@ export const NotificationPopup: React.FC = (props) => { popupHeight: props.popupHeight || 600, buttonIconSize: props.buttonIconSize || (props.buttonWidth ? props.buttonWidth / 2 : 20), - imageShape: props.imageShape || "circle", - pagination: props.pagination || "INFINITE_SCROLL", + imageShape: props.imageShape || 'circle', + pagination: props.pagination || 'INFINITE_SCROLL', pageSize: props.pageSize || 10, - pagePosition: props.pagePosition || "top", + pagePosition: props.pagePosition || 'top', style: props.style || {}, unreadBadgeProps: props.unreadBadgeProps ?? {}, - count: props.count || "COUNT_UNOPENED_NOTIFICATIONS", - filter: props.filter || Filter.ALL, + count: props.count || 'COUNT_UNOPENED_NOTIFICATIONS', + filter: props.filter || Filter.ALL }; const context = useContext(NotificationAPIContext); @@ -72,20 +72,20 @@ export const NotificationPopup: React.FC = (props) => { }} arrow={false} overlayStyle={{ - padding: "0 16px", - minWidth: config.popupWidth, + padding: '0 16px', + minWidth: config.popupWidth }} >
= (props) => { icon={ } style={{ width: config.buttonWidth, - height: config.buttonHeight, + height: config.buttonHeight }} shape="circle" type="text" diff --git a/lib/components/Notifications/UnreadBadge.tsx b/lib/components/Notifications/UnreadBadge.tsx index f7dd1e7..04892be 100644 --- a/lib/components/Notifications/UnreadBadge.tsx +++ b/lib/components/Notifications/UnreadBadge.tsx @@ -1,36 +1,36 @@ -import { Badge } from "antd"; -import { PropsWithChildren, useContext } from "react"; -import { NotificationAPIContext } from "../Provider"; -import { NotificationPopupProps } from "./NotificationPopup"; +import { Badge } from 'antd'; +import { PropsWithChildren, useContext } from 'react'; +import { NotificationAPIContext } from '../Provider'; +import { NotificationPopupProps } from './NotificationPopup'; export type UnreadBadgeProps = { color?: - | "blue" - | "purple" - | "cyan" - | "green" - | "magenta" - | "pink" - | "red" - | "orange" - | "yellow" - | "volcano" - | "geekblue" - | "lime" - | "gold" + | 'blue' + | 'purple' + | 'cyan' + | 'green' + | 'magenta' + | 'pink' + | 'red' + | 'orange' + | 'yellow' + | 'volcano' + | 'geekblue' + | 'lime' + | 'gold' | undefined; overflowCount?: number; dot?: boolean; showZero?: boolean; - size?: "default" | "small"; + size?: 'default' | 'small'; style?: React.CSSProperties; count?: keyof typeof COUNT_TYPE | ((notification: any) => boolean); - filter?: NotificationPopupProps["filter"]; + filter?: NotificationPopupProps['filter']; }; export enum COUNT_TYPE { - COUNT_UNOPENED_NOTIFICATIONS = "COUNT_UNOPENED_NOTIFICATIONS", - COUNT_UNARCHIVED_NOTIFICATIONS = "COUNT_UNARCHIVED_NOTIFICATIONS", + COUNT_UNOPENED_NOTIFICATIONS = 'COUNT_UNOPENED_NOTIFICATIONS', + COUNT_UNARCHIVED_NOTIFICATIONS = 'COUNT_UNARCHIVED_NOTIFICATIONS' } export const UnreadBadge: React.FunctionComponent< @@ -40,11 +40,11 @@ export const UnreadBadge: React.FunctionComponent< const countingFunction = (notifications: any[]) => { if ( - props.count === "COUNT_UNOPENED_NOTIFICATIONS" || + props.count === 'COUNT_UNOPENED_NOTIFICATIONS' || props.count === undefined ) { return notifications.filter((n) => !n.opened && !n.seen).length; - } else if (props.count === "COUNT_UNARCHIVED_NOTIFICATIONS") { + } else if (props.count === 'COUNT_UNARCHIVED_NOTIFICATIONS') { return notifications.filter( (n) => !n.archived && @@ -67,7 +67,7 @@ export const UnreadBadge: React.FunctionComponent< showZero={props.showZero} size={props.size} style={{ - ...props.style, + ...props.style }} > {props.children} diff --git a/lib/components/Notifications/index.tsx b/lib/components/Notifications/index.tsx index 5b3f062..284450d 100644 --- a/lib/components/Notifications/index.tsx +++ b/lib/components/Notifications/index.tsx @@ -1,4 +1,4 @@ -export { NotificationFeed } from "./NotificationFeed"; -export { NotificationPopup } from "./NotificationPopup"; -export { NotificationLauncher } from "./NotificationLauncher"; -export { NotificationCounter } from "./NotificationCounter"; +export { NotificationFeed } from './NotificationFeed'; +export { NotificationPopup } from './NotificationPopup'; +export { NotificationLauncher } from './NotificationLauncher'; +export { NotificationCounter } from './NotificationCounter'; diff --git a/lib/components/Preferences/NotificationPreferencesPopup.tsx b/lib/components/Preferences/NotificationPreferencesPopup.tsx index bbfc9fc..4588562 100644 --- a/lib/components/Preferences/NotificationPreferencesPopup.tsx +++ b/lib/components/Preferences/NotificationPreferencesPopup.tsx @@ -1,5 +1,5 @@ -import { Modal } from "antd"; -import { Preferences } from "./Preferences"; +import { Modal } from 'antd'; +import { Preferences } from './Preferences'; type NotificationPreferencesPopupProps = { open?: boolean; @@ -11,7 +11,7 @@ export function NotificationPreferencesPopup( ) { const config: Required = { open: props.open === undefined ? true : props.open, - onClose: props.onClose || (() => {}), + onClose: props.onClose || (() => {}) }; return ( diff --git a/lib/components/Preferences/PreferenceGroup.tsx b/lib/components/Preferences/PreferenceGroup.tsx index 224dfd8..0d8dde1 100644 --- a/lib/components/Preferences/PreferenceGroup.tsx +++ b/lib/components/Preferences/PreferenceGroup.tsx @@ -5,11 +5,11 @@ import { Radio, Space, Switch, - Typography, -} from "antd"; -import { Channels, DeliveryOptions, NotificationAPIContext } from "../Provider"; -import { getChannelIcon, getChannelLabel } from "./Preferences"; -import { useContext } from "react"; + Typography +} from 'antd'; +import { Channels, DeliveryOptions, NotificationAPIContext } from '../Provider'; +import { getChannelIcon, getChannelLabel } from './Preferences'; +import { useContext } from 'react'; const Text = Typography.Text; @@ -20,12 +20,12 @@ type PreferenceGroupProps = { const getDeliveryLabel = (d: DeliveryOptions) => { const labels = { - off: "Off", - instant: "Instant", - hourly: "Hourly", - daily: "Daily", - weekly: "Weekly", - monthly: "Monthly", + off: 'Off', + instant: 'Instant', + hourly: 'Hourly', + daily: 'Daily', + weekly: 'Weekly', + monthly: 'Monthly' }; return labels[d]; }; @@ -37,7 +37,7 @@ const sortChannels = (a: Channels, b: Channels) => { Channels.SMS, Channels.CALL, Channels.PUSH, - Channels.WEB_PUSH, + Channels.WEB_PUSH ]; return order.indexOf(a) - order.indexOf(b); }; @@ -49,7 +49,7 @@ const sortDeliveries = (a: DeliveryOptions, b: DeliveryOptions) => { DeliveryOptions.HOURLY, DeliveryOptions.DAILY, DeliveryOptions.WEEKLY, - DeliveryOptions.MONTHLY, + DeliveryOptions.MONTHLY ]; return order.indexOf(a) - order.indexOf(b); }; @@ -64,7 +64,7 @@ export const PreferenceGroup: React.FC = () => { const prefs = context.preferences.preferences; const notifications = context.preferences.notifications; - const items: CollapseProps["items"] = notifications.map((n) => { + const items: CollapseProps['items'] = notifications.map((n) => { const onChannels = Array.from( new Set( prefs @@ -81,7 +81,7 @@ export const PreferenceGroup: React.FC = () => { key: n.notificationId, extra: ( - {onChannels.map((c) => getChannelLabel(c)).join(", ")} + {onChannels.map((c) => getChannelLabel(c)).join(', ')} ), children: ( @@ -97,7 +97,7 @@ export const PreferenceGroup: React.FC = () => { const deliveries = Object.keys(n.options![c]!).filter( (o) => - o !== "defaultDeliveryOption" && o !== "defaultDeliverOption" + o !== 'defaultDeliveryOption' && o !== 'defaultDeliverOption' ) as DeliveryOptions[]; let selector; @@ -148,12 +148,12 @@ export const PreferenceGroup: React.FC = () => { />
@@ -194,11 +194,11 @@ export const PreferenceGroup: React.FC = () => {
@@ -212,7 +212,7 @@ export const PreferenceGroup: React.FC = () => { } })}
- ), + ) }; }); diff --git a/lib/components/Preferences/Preferences.tsx b/lib/components/Preferences/Preferences.tsx index 0d83b35..7a28857 100644 --- a/lib/components/Preferences/Preferences.tsx +++ b/lib/components/Preferences/Preferences.tsx @@ -1,27 +1,27 @@ -import { Typography } from "antd"; -import { Channels, NotificationAPIContext } from "../Provider"; -import { useContext } from "react"; +import { Typography } from 'antd'; +import { Channels, NotificationAPIContext } from '../Provider'; +import { useContext } from 'react'; import { BellOutlined, ChromeOutlined, MailOutlined, MessageOutlined, MobileOutlined, - PhoneOutlined, -} from "@ant-design/icons"; -import { blue } from "@ant-design/colors"; -import { PreferenceGroup } from "./PreferenceGroup"; + PhoneOutlined +} from '@ant-design/icons'; +import { blue } from '@ant-design/colors'; +import { PreferenceGroup } from './PreferenceGroup'; const Text = Typography.Text; export const getChannelLabel = (c: Channels) => { const labels = { - EMAIL: "Email", - INAPP_WEB: "In-App", - SMS: "Text", - CALL: "Automated Calling", - PUSH: "Mobile", - WEB_PUSH: "Browser", + EMAIL: 'Email', + INAPP_WEB: 'In-App', + SMS: 'Text', + CALL: 'Automated Calling', + PUSH: 'Mobile', + WEB_PUSH: 'Browser' }; return labels[c]; }; @@ -67,7 +67,7 @@ export function Preferences() { ) { uniqueSubNotifications.push({ subNotificationId: sn.subNotificationId, - title: sn.title, + title: sn.title }); } }); diff --git a/lib/components/Preferences/index.tsx b/lib/components/Preferences/index.tsx index a671142..da4e708 100644 --- a/lib/components/Preferences/index.tsx +++ b/lib/components/Preferences/index.tsx @@ -1,2 +1,2 @@ -export { NotificationPreferencesPopup } from "./NotificationPreferencesPopup"; -export { NotificationPreferencesInline } from "./NotificationPreferencesInline"; +export { NotificationPreferencesPopup } from './NotificationPreferencesPopup'; +export { NotificationPreferencesInline } from './NotificationPreferencesInline'; diff --git a/lib/components/Provider/index.tsx b/lib/components/Provider/index.tsx index 075147d..ca413c4 100644 --- a/lib/components/Provider/index.tsx +++ b/lib/components/Provider/index.tsx @@ -2,9 +2,9 @@ import React, { PropsWithChildren, createContext, useEffect, - useState, -} from "react"; -import { api } from "../../api"; + useState +} from 'react'; +import { api } from '../../api'; type Props = { clientId: string; @@ -16,37 +16,37 @@ type Props = { }; interface WS_NewNotification { - route: "inapp_web/new_notifications"; + route: 'inapp_web/new_notifications'; payload: { notifications: any[]; }; } export const NOTIFICATION_ACTIONS = { - opened: "opened", - clicked: "clicked", - archived: "archived", - replied: "replied", - actioned1: "actioned1", - actioned2: "actioned2", + opened: 'opened', + clicked: 'clicked', + archived: 'archived', + replied: 'replied', + actioned1: 'actioned1', + actioned2: 'actioned2' }; export enum Channels { - EMAIL = "EMAIL", - INAPP_WEB = "INAPP_WEB", - SMS = "SMS", - CALL = "CALL", - PUSH = "PUSH", - WEB_PUSH = "WEB_PUSH", + EMAIL = 'EMAIL', + INAPP_WEB = 'INAPP_WEB', + SMS = 'SMS', + CALL = 'CALL', + PUSH = 'PUSH', + WEB_PUSH = 'WEB_PUSH' } export enum DeliveryOptions { - OFF = "off", - INSTANT = "instant", - HOURLY = "hourly", - DAILY = "daily", - WEEKLY = "weekly", - MONTHLY = "monthly", + OFF = 'off', + INSTANT = 'instant', + HOURLY = 'hourly', + DAILY = 'daily', + WEEKLY = 'weekly', + MONTHLY = 'monthly' } export interface Notification { @@ -61,9 +61,9 @@ export interface Notification { throttling?: { max: number; period: number; - unit: "seconds" | "minutes" | "hours" | "days" | "months" | "years"; + unit: 'seconds' | 'minutes' | 'hours' | 'days' | 'months' | 'years'; forever: boolean; - scope: ["userId", "notificationId"]; + scope: ['userId', 'notificationId']; }; retention?: number; options?: { @@ -90,7 +90,7 @@ export interface Notification { [DeliveryOptions.MONTHLY]: { enabled: boolean; hour: string; - date: "first" | "last"; + date: 'first' | 'last'; }; }; }; @@ -127,7 +127,7 @@ export interface Preferences { notificationId: string; title: string; channels: Channels[]; - options: Notification["options"]; + options: Notification['options']; }[]; subNotifications: { notificationId: string; @@ -141,7 +141,7 @@ export type Context = { preferences?: Preferences; loadNotifications: (initial?: boolean) => void; markAsOpened: () => void; - markAsArchived: (ids: string[] | "ALL") => void; + markAsArchived: (ids: string[] | 'ALL') => void; markAsClicked: (id: string) => void; updateDelivery: ( notificationId: string, @@ -159,15 +159,15 @@ export const NotificatinAPIProvider: React.FunctionComponent< PropsWithChildren > = (props) => { const defaultConfigs = { - apiURL: "https://api.notificationapi.com", - wsURL: "wss://ws.notificationapi.com", + apiURL: 'https://api.notificationapi.com', + wsURL: 'wss://ws.notificationapi.com', initialLoadMaxCount: 1000, - initialLoadMaxAge: new Date(new Date().setMonth(new Date().getMonth() - 3)), + initialLoadMaxAge: new Date(new Date().setMonth(new Date().getMonth() - 3)) }; const config = { ...defaultConfigs, - ...props, + ...props }; const [notifications, setNotifications] = useState(); @@ -184,7 +184,7 @@ export const NotificatinAPIProvider: React.FunctionComponent< ...notis.filter((n) => { return !prev.find((p) => p.id === n.id); }), - ...prev, + ...prev ]; }); }; @@ -195,7 +195,7 @@ export const NotificatinAPIProvider: React.FunctionComponent< ): Promise => { const res = await api( config.apiURL, - "GET", + 'GET', `notifications/INAPP_WEB?count=${count}&before=${before}`, props.clientId, props.userId @@ -245,7 +245,7 @@ export const NotificatinAPIProvider: React.FunctionComponent< return { notifications: result, couldLoadMore, - oldestReceived, + oldestReceived }; }; @@ -268,14 +268,14 @@ export const NotificatinAPIProvider: React.FunctionComponent< const date = new Date().toISOString(); api( config.apiURL, - "PATCH", + 'PATCH', `notifications/INAPP_WEB`, props.clientId, props.userId, - "", + '', { trackingIds: [id], - clicked: date, + clicked: date } ); @@ -302,14 +302,14 @@ export const NotificatinAPIProvider: React.FunctionComponent< api( config.apiURL, - "PATCH", + 'PATCH', `notifications/INAPP_WEB`, props.clientId, props.userId, - "", + '', { trackingIds, - opened: date, + opened: date } ); @@ -326,13 +326,13 @@ export const NotificatinAPIProvider: React.FunctionComponent< }); }; - const markAsArchived = async (ids: string[] | "ALL") => { + const markAsArchived = async (ids: string[] | 'ALL') => { if (!notifications) return; const date = new Date().toISOString(); const trackingIds: string[] = notifications .filter((n) => { - return !n.archived && (ids === "ALL" || ids.includes(n.id)); + return !n.archived && (ids === 'ALL' || ids.includes(n.id)); }) .map((n) => n.id); @@ -340,14 +340,14 @@ export const NotificatinAPIProvider: React.FunctionComponent< api( config.apiURL, - "PATCH", + 'PATCH', `notifications/INAPP_WEB`, props.clientId, props.userId, - "", + '', { trackingIds, - archived: date, + archived: date } ); @@ -394,13 +394,13 @@ export const NotificatinAPIProvider: React.FunctionComponent< return; } - if (body.route === "inapp_web/new_notifications") { + if (body.route === 'inapp_web/new_notifications') { const message = body as WS_NewNotification; addNotificationsToState(message.payload.notifications); } }; - api(config.apiURL, "GET", `preferences`, props.clientId, props.userId).then( + api(config.apiURL, 'GET', `preferences`, props.clientId, props.userId).then( (res) => { setPreferences(res); } @@ -414,7 +414,7 @@ export const NotificatinAPIProvider: React.FunctionComponent< markAsOpened, markAsArchived, markAsClicked, - updateDelivery, + updateDelivery }; return ( diff --git a/lib/main.ts b/lib/main.ts index c1545b6..be0e69e 100644 --- a/lib/main.ts +++ b/lib/main.ts @@ -2,10 +2,10 @@ export { NotificationFeed, NotificationPopup, NotificationLauncher, - NotificationCounter, -} from "./components/Notifications"; + NotificationCounter +} from './components/Notifications'; export { NotificationPreferencesInline, - NotificationPreferencesPopup, -} from "./components/Preferences"; -export { NotificatinAPIProvider } from "./components/Provider"; + NotificationPreferencesPopup +} from './components/Preferences'; +export { NotificatinAPIProvider } from './components/Provider'; diff --git a/package-lock.json b/package-lock.json index 34a5810..feba812 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", + "prettier": "^3.3.3", "typescript": "^5.2.2", "vite": "^5.2.0", "vite-plugin-dts": "^3.9.1" @@ -3752,6 +3753,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/package.json b/package.json index 7f631e1..c07b8a7 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "dev": "vite", "build": "tsc --p ./tsconfig.build.json && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "format": "prettier --write \"{src,lib}/**/*.{ts,tsx,js,jsx,json,css,md,html}\"", + "prettier-check": "prettier --check \"{src,lib}/**/*.{ts,tsx,js,jsx,json,css,md,html}\"", "preview": "vite preview", "prepublishOnly": "npm run build" }, @@ -25,6 +27,7 @@ "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", + "prettier": "^3.3.3", "typescript": "^5.2.2", "vite": "^5.2.0", "vite-plugin-dts": "^3.9.1" diff --git a/src/App.tsx b/src/App.tsx index ff31a00..44abd9a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,19 +1,19 @@ -import { Button, Divider } from "antd"; +import { Button, Divider } from 'antd'; import { NotificationFeed, NotificationPopup, NotificationLauncher, NotificationCounter, - NotificatinAPIProvider, -} from "../lib/main"; + NotificatinAPIProvider +} from '../lib/main'; function App() { return (
From 779690ad88c70e6877bb4e763a85398a57fbea4b Mon Sep 17 00:00:00 2001 From: mbasadi Date: Mon, 12 Aug 2024 16:55:00 -0400 Subject: [PATCH 02/12] Lint errors --- lib/api.ts | 4 +-- lib/components/Notifications/Inbox.tsx | 8 ++--- lib/components/Notifications/Notification.tsx | 3 +- .../Notifications/NotificationFeed.tsx | 18 ++++++----- .../Notifications/NotificationPopup.tsx | 4 +-- lib/components/Notifications/UnreadBadge.tsx | 10 ++++--- .../Preferences/PreferenceGroup.tsx | 4 +-- lib/components/Provider/index.tsx | 30 ++++++++++++------- 8 files changed, 47 insertions(+), 34 deletions(-) diff --git a/lib/api.ts b/lib/api.ts index 4d88e63..5da4cd6 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -5,8 +5,8 @@ export const api = async ( clientId: string, userId: string, hashedUserId?: string, - data?: any -): Promise => { + data?: unknown +): Promise => { const token = hashedUserId ? btoa(clientId + ':' + userId + ':' + hashedUserId) : btoa(clientId + ':' + userId); diff --git a/lib/components/Notifications/Inbox.tsx b/lib/components/Notifications/Inbox.tsx index 3ca6261..2c7bd55 100644 --- a/lib/components/Notifications/Inbox.tsx +++ b/lib/components/Notifications/Inbox.tsx @@ -2,7 +2,7 @@ import { Empty, List } from 'antd'; import { InboxHeader } from './InboxHeader'; import VirtualList from 'rc-virtual-list'; import { ImageShape, Notification } from './Notification'; -import { NotificationAPIContext } from '../Provider'; +import { InappNotification, NotificationAPIContext } from '../Provider'; import { useContext } from 'react'; import { Filter, NotificationPopupProps } from './NotificationPopup'; @@ -23,7 +23,7 @@ type InboxProps = { export const Inbox: React.FC = (props) => { const context = useContext(NotificationAPIContext); - const filterFunction = (notifications: any[]) => { + const filterFunction = (notifications: InappNotification[]) => { if (props.filter === Filter.ALL || !props.filter) { return notifications; } else if (props.filter === Filter.UNARCHIVED) { @@ -70,7 +70,7 @@ export const Inbox: React.FC = (props) => { } }} > - {(n: any) => ( + {(n: InappNotification) => ( = (props) => { } dataSource={filterFunction(context.notifications)} - renderItem={(n: any) => ( + renderItem={(n: InappNotification) => ( void; markAsClicked: (id: string) => void; imageShape: keyof typeof ImageShape; diff --git a/lib/components/Notifications/NotificationFeed.tsx b/lib/components/Notifications/NotificationFeed.tsx index e1eb163..45be2e1 100644 --- a/lib/components/Notifications/NotificationFeed.tsx +++ b/lib/components/Notifications/NotificationFeed.tsx @@ -1,7 +1,7 @@ import { useContext, useEffect } from 'react'; import { Inbox, Pagination } from './Inbox'; import { ImageShape } from './Notification'; -import { NotificationAPIContext } from '../Provider'; +import { InappNotification, NotificationAPIContext } from '../Provider'; import { Filter } from './NotificationPopup'; export type NotificationFeedProps = { @@ -11,7 +11,7 @@ export type NotificationFeedProps = { pagePosition?: 'top' | 'bottom'; infiniteScrollHeight?: number; style?: React.CSSProperties; - filter?: keyof typeof Filter | ((n: any) => boolean); + filter?: keyof typeof Filter | ((n: InappNotification) => boolean); }; export const NotificationFeed: React.FC = (props) => { @@ -29,18 +29,20 @@ export const NotificationFeed: React.FC = (props) => { const context = useContext(NotificationAPIContext); - if (!context) { - return null; - } - - // every 5 seconds useEffect(() => { + if (!context) return; + context.markAsOpened(); const interval = setInterval(() => { context.markAsOpened(); }, 5000); + return () => clearInterval(interval); - }, []); + }, [context]); + + if (!context) { + return null; + } return (
boolean); + filter?: keyof typeof Filter | ((n: InappNotification) => boolean); }; export const NotificationPopup: React.FC = (props) => { diff --git a/lib/components/Notifications/UnreadBadge.tsx b/lib/components/Notifications/UnreadBadge.tsx index 04892be..2fc4a97 100644 --- a/lib/components/Notifications/UnreadBadge.tsx +++ b/lib/components/Notifications/UnreadBadge.tsx @@ -1,6 +1,6 @@ import { Badge } from 'antd'; import { PropsWithChildren, useContext } from 'react'; -import { NotificationAPIContext } from '../Provider'; +import { InappNotification, NotificationAPIContext } from '../Provider'; import { NotificationPopupProps } from './NotificationPopup'; export type UnreadBadgeProps = { @@ -24,7 +24,9 @@ export type UnreadBadgeProps = { showZero?: boolean; size?: 'default' | 'small'; style?: React.CSSProperties; - count?: keyof typeof COUNT_TYPE | ((notification: any) => boolean); + count?: + | keyof typeof COUNT_TYPE + | ((notification: InappNotification) => boolean); filter?: NotificationPopupProps['filter']; }; @@ -38,7 +40,7 @@ export const UnreadBadge: React.FunctionComponent< > = (props) => { const context = useContext(NotificationAPIContext); - const countingFunction = (notifications: any[]) => { + const countingFunction = (notifications: InappNotification[]) => { if ( props.count === 'COUNT_UNOPENED_NOTIFICATIONS' || props.count === undefined @@ -49,7 +51,7 @@ export const UnreadBadge: React.FunctionComponent< (n) => !n.archived && !n.clicked && - !n.replied && + !n.replies && !n.actioned1 && !n.actioned2 ).length; diff --git a/lib/components/Preferences/PreferenceGroup.tsx b/lib/components/Preferences/PreferenceGroup.tsx index 0d8dde1..e3484f8 100644 --- a/lib/components/Preferences/PreferenceGroup.tsx +++ b/lib/components/Preferences/PreferenceGroup.tsx @@ -92,8 +92,8 @@ export const PreferenceGroup: React.FC = () => { )!; if (p.notificationId === n.notificationId) { - let name = getChannelLabel(c); - let icon = getChannelIcon(c); + const name = getChannelLabel(c); + const icon = getChannelIcon(c); const deliveries = Object.keys(n.options![c]!).filter( (o) => diff --git a/lib/components/Provider/index.tsx b/lib/components/Provider/index.tsx index ca413c4..3a1918f 100644 --- a/lib/components/Provider/index.tsx +++ b/lib/components/Provider/index.tsx @@ -18,7 +18,7 @@ type Props = { interface WS_NewNotification { route: 'inapp_web/new_notifications'; payload: { - notifications: any[]; + notifications: InappNotification[]; }; } @@ -102,7 +102,7 @@ export interface InappNotification { title: string; redirectURL?: string; imageURL?: string; - date: Date; + date: string; parameters?: Record; expDate?: Date; opened?: string; @@ -176,7 +176,7 @@ export const NotificatinAPIProvider: React.FunctionComponent< const [oldestLoaded, setOldestLoaded] = useState(new Date().toISOString()); const [hasMore, setHasMore] = useState(true); - const addNotificationsToState = (notis: any[]) => { + const addNotificationsToState = (notis: InappNotification[]) => { setNotifications((prev) => { if (!prev) return notis; // if no existing notifications in state, just return the new ones @@ -192,7 +192,7 @@ export const NotificatinAPIProvider: React.FunctionComponent< const getNotifications = async ( count: number, before: number - ): Promise => { + ): Promise => { const res = await api( config.apiURL, 'GET', @@ -200,7 +200,7 @@ export const NotificatinAPIProvider: React.FunctionComponent< props.clientId, props.userId ); - return res.notifications; + return (res as { notifications: InappNotification[] }).notifications; }; const fetchNotificationsBeforeDate = async ( @@ -208,11 +208,11 @@ export const NotificatinAPIProvider: React.FunctionComponent< maxCountNeeded: number, oldestNeeded?: string ): Promise<{ - notifications: any[]; + notifications: InappNotification[]; couldLoadMore: boolean; oldestReceived: string; }> => { - let result: any[] = []; + let result: InappNotification[] = []; let oldestReceived = before; let couldLoadMore = true; let shouldLoadMore = true; @@ -222,10 +222,10 @@ export const NotificatinAPIProvider: React.FunctionComponent< new Date(oldestReceived).getTime() ); const notisWithoutDuplicates = notis.filter( - (n: any) => !result.find((nn) => nn.id === n.id) + (n: InappNotification) => !result.find((nn) => nn.id === n.id) ); oldestReceived = notisWithoutDuplicates.reduce( - (min: string, n: any) => (min < n.date ? min : n.date), + (min: string, n: InappNotification) => (min < n.date ? min : n.date), before ); result = [...result, ...notisWithoutDuplicates]; @@ -402,10 +402,18 @@ export const NotificatinAPIProvider: React.FunctionComponent< api(config.apiURL, 'GET', `preferences`, props.clientId, props.userId).then( (res) => { - setPreferences(res); + setPreferences(res as Preferences); } ); - }, []); + }, [ + config.apiURL, + config.clientId, + config.userId, + config.wsURL, + loadNotifications, + props.clientId, + props.userId + ]); const value: Context = { notifications, From 8d22751aae3f19d69934be0bd585c991b59dddd0 Mon Sep 17 00:00:00 2001 From: mbasadi Date: Mon, 12 Aug 2024 23:35:50 -0400 Subject: [PATCH 03/12] Lint warnings: - Move shared enums and a constant to a file to solve this lint warning: warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components --- lib/components/Notifications/Inbox.tsx | 10 ++-- lib/components/Notifications/Notification.tsx | 6 +-- .../Notifications/NotificationFeed.tsx | 5 +- .../Notifications/NotificationLauncher.tsx | 8 +-- .../Notifications/NotificationPopup.tsx | 9 +--- lib/components/Notifications/UnreadBadge.tsx | 6 +-- lib/components/Notifications/interface.ts | 53 +++++++++++++++++++ .../Preferences/PreferenceGroup.tsx | 5 +- lib/components/Preferences/Preferences.tsx | 42 +-------------- lib/components/Preferences/channelUtils.tsx | 41 ++++++++++++++ lib/components/Provider/index.tsx | 28 +--------- 11 files changed, 109 insertions(+), 104 deletions(-) create mode 100644 lib/components/Notifications/interface.ts create mode 100644 lib/components/Preferences/channelUtils.tsx diff --git a/lib/components/Notifications/Inbox.tsx b/lib/components/Notifications/Inbox.tsx index 2c7bd55..740c54a 100644 --- a/lib/components/Notifications/Inbox.tsx +++ b/lib/components/Notifications/Inbox.tsx @@ -1,15 +1,11 @@ import { Empty, List } from 'antd'; import { InboxHeader } from './InboxHeader'; import VirtualList from 'rc-virtual-list'; -import { ImageShape, Notification } from './Notification'; +import { Notification } from './Notification'; import { InappNotification, NotificationAPIContext } from '../Provider'; import { useContext } from 'react'; -import { Filter, NotificationPopupProps } from './NotificationPopup'; - -export enum Pagination { - INFINITE_SCROLL = 'infinite_scroll', - PAGINATED = 'paginated' -} +import { NotificationPopupProps } from './NotificationPopup'; +import { Filter, ImageShape, Pagination } from './interface'; type InboxProps = { pagination: keyof typeof Pagination; diff --git a/lib/components/Notifications/Notification.tsx b/lib/components/Notifications/Notification.tsx index 2d6e198..53d0566 100644 --- a/lib/components/Notifications/Notification.tsx +++ b/lib/components/Notifications/Notification.tsx @@ -5,15 +5,11 @@ import TimeAgo from 'javascript-time-ago'; import en from 'javascript-time-ago/locale/en'; import ReactTimeAgo from 'react-time-ago'; import { InappNotification } from '../Provider'; +import { ImageShape } from './interface'; TimeAgo.addDefaultLocale(en); TimeAgo.addLocale(en); -export enum ImageShape { - square = 'square', - circle = 'circle' -} - const NotificationDiv = styled.div<{ $redirect: boolean; $seen: boolean; diff --git a/lib/components/Notifications/NotificationFeed.tsx b/lib/components/Notifications/NotificationFeed.tsx index 45be2e1..b5c0d73 100644 --- a/lib/components/Notifications/NotificationFeed.tsx +++ b/lib/components/Notifications/NotificationFeed.tsx @@ -1,8 +1,7 @@ import { useContext, useEffect } from 'react'; -import { Inbox, Pagination } from './Inbox'; -import { ImageShape } from './Notification'; +import { Inbox } from './Inbox'; import { InappNotification, NotificationAPIContext } from '../Provider'; -import { Filter } from './NotificationPopup'; +import { Filter, ImageShape, Pagination } from './interface'; export type NotificationFeedProps = { imageShape?: keyof typeof ImageShape; diff --git a/lib/components/Notifications/NotificationLauncher.tsx b/lib/components/Notifications/NotificationLauncher.tsx index 4e665d1..79cca88 100644 --- a/lib/components/Notifications/NotificationLauncher.tsx +++ b/lib/components/Notifications/NotificationLauncher.tsx @@ -5,13 +5,7 @@ import { UnreadBadge } from './UnreadBadge'; import { NotificationPopupProps } from './NotificationPopup'; import { useContext } from 'react'; import { NotificationAPIContext } from '../Provider'; - -export enum Position { - TOP_LEFT = 'top-left', - TOP_RIGHT = 'top-right', - BOTTOM_LEFT = 'bottom-left', - BOTTOM_RIGHT = 'bottom-right' -} +import { Position } from './interface'; type NotificationLaucherProps = NotificationPopupProps & { position?: keyof typeof Position; diff --git a/lib/components/Notifications/NotificationPopup.tsx b/lib/components/Notifications/NotificationPopup.tsx index 631d067..a8b46c3 100644 --- a/lib/components/Notifications/NotificationPopup.tsx +++ b/lib/components/Notifications/NotificationPopup.tsx @@ -1,15 +1,10 @@ import { Button, Popover } from 'antd'; -import { Inbox, Pagination } from './Inbox'; +import { Inbox } from './Inbox'; import { BellOutlined } from '@ant-design/icons'; import { UnreadBadge, UnreadBadgeProps } from './UnreadBadge'; -import { ImageShape } from './Notification'; import { InappNotification, NotificationAPIContext } from '../Provider'; import { useContext } from 'react'; - -export enum Filter { - ALL = 'ALL', - UNARCHIVED = 'UNARCHIVED' -} +import { Filter, ImageShape, Pagination } from './interface'; export type NotificationPopupProps = { buttonIconSize?: number; diff --git a/lib/components/Notifications/UnreadBadge.tsx b/lib/components/Notifications/UnreadBadge.tsx index 2fc4a97..561fbd6 100644 --- a/lib/components/Notifications/UnreadBadge.tsx +++ b/lib/components/Notifications/UnreadBadge.tsx @@ -2,6 +2,7 @@ import { Badge } from 'antd'; import { PropsWithChildren, useContext } from 'react'; import { InappNotification, NotificationAPIContext } from '../Provider'; import { NotificationPopupProps } from './NotificationPopup'; +import { COUNT_TYPE } from './interface'; export type UnreadBadgeProps = { color?: @@ -30,11 +31,6 @@ export type UnreadBadgeProps = { filter?: NotificationPopupProps['filter']; }; -export enum COUNT_TYPE { - COUNT_UNOPENED_NOTIFICATIONS = 'COUNT_UNOPENED_NOTIFICATIONS', - COUNT_UNARCHIVED_NOTIFICATIONS = 'COUNT_UNARCHIVED_NOTIFICATIONS' -} - export const UnreadBadge: React.FunctionComponent< PropsWithChildren > = (props) => { diff --git a/lib/components/Notifications/interface.ts b/lib/components/Notifications/interface.ts new file mode 100644 index 0000000..d627059 --- /dev/null +++ b/lib/components/Notifications/interface.ts @@ -0,0 +1,53 @@ +export enum Pagination { + INFINITE_SCROLL = 'infinite_scroll', + PAGINATED = 'paginated' +} + +export enum ImageShape { + square = 'square', + circle = 'circle' +} + +export enum Filter { + ALL = 'ALL', + UNARCHIVED = 'UNARCHIVED' +} + +export enum Channels { + EMAIL = 'EMAIL', + INAPP_WEB = 'INAPP_WEB', + SMS = 'SMS', + CALL = 'CALL', + PUSH = 'PUSH', + WEB_PUSH = 'WEB_PUSH' +} + +export enum DeliveryOptions { + OFF = 'off', + INSTANT = 'instant', + HOURLY = 'hourly', + DAILY = 'daily', + WEEKLY = 'weekly', + MONTHLY = 'monthly' +} + +export enum COUNT_TYPE { + COUNT_UNOPENED_NOTIFICATIONS = 'COUNT_UNOPENED_NOTIFICATIONS', + COUNT_UNARCHIVED_NOTIFICATIONS = 'COUNT_UNARCHIVED_NOTIFICATIONS' +} + +export enum Position { + TOP_LEFT = 'top-left', + TOP_RIGHT = 'top-right', + BOTTOM_LEFT = 'bottom-left', + BOTTOM_RIGHT = 'bottom-right' +} + +export const NOTIFICATION_ACTIONS = { + opened: 'opened', + clicked: 'clicked', + archived: 'archived', + replied: 'replied', + actioned1: 'actioned1', + actioned2: 'actioned2' +}; diff --git a/lib/components/Preferences/PreferenceGroup.tsx b/lib/components/Preferences/PreferenceGroup.tsx index e3484f8..92da091 100644 --- a/lib/components/Preferences/PreferenceGroup.tsx +++ b/lib/components/Preferences/PreferenceGroup.tsx @@ -7,9 +7,10 @@ import { Switch, Typography } from 'antd'; -import { Channels, DeliveryOptions, NotificationAPIContext } from '../Provider'; -import { getChannelIcon, getChannelLabel } from './Preferences'; +import { NotificationAPIContext } from '../Provider'; import { useContext } from 'react'; +import { Channels, DeliveryOptions } from '../Notifications/interface'; +import { getChannelIcon, getChannelLabel } from './channelUtils'; const Text = Typography.Text; diff --git a/lib/components/Preferences/Preferences.tsx b/lib/components/Preferences/Preferences.tsx index 7a28857..dffa72e 100644 --- a/lib/components/Preferences/Preferences.tsx +++ b/lib/components/Preferences/Preferences.tsx @@ -1,50 +1,10 @@ import { Typography } from 'antd'; -import { Channels, NotificationAPIContext } from '../Provider'; +import { NotificationAPIContext } from '../Provider'; import { useContext } from 'react'; -import { - BellOutlined, - ChromeOutlined, - MailOutlined, - MessageOutlined, - MobileOutlined, - PhoneOutlined -} from '@ant-design/icons'; -import { blue } from '@ant-design/colors'; import { PreferenceGroup } from './PreferenceGroup'; const Text = Typography.Text; -export const getChannelLabel = (c: Channels) => { - const labels = { - EMAIL: 'Email', - INAPP_WEB: 'In-App', - SMS: 'Text', - CALL: 'Automated Calling', - PUSH: 'Mobile', - WEB_PUSH: 'Browser' - }; - return labels[c]; -}; - -export const getChannelIcon = (channel: Channels): React.ReactElement => { - switch (channel) { - case Channels.EMAIL: - return ; - case Channels.SMS: - return ; - case Channels.PUSH: - return ; - case Channels.CALL: - return ; - case Channels.INAPP_WEB: - return ; - case Channels.WEB_PUSH: - return ; - default: - return ; - } -}; - export function Preferences() { const context = useContext(NotificationAPIContext); diff --git a/lib/components/Preferences/channelUtils.tsx b/lib/components/Preferences/channelUtils.tsx new file mode 100644 index 0000000..93ce13d --- /dev/null +++ b/lib/components/Preferences/channelUtils.tsx @@ -0,0 +1,41 @@ +import { + MailOutlined, + MessageOutlined, + MobileOutlined, + PhoneOutlined, + BellOutlined, + ChromeOutlined +} from '@ant-design/icons'; +import { blue } from '@ant-design/colors'; +import { Channels } from '../Notifications/interface'; + +export const getChannelLabel = (c: Channels): string => { + const labels: Record = { + EMAIL: 'Email', + INAPP_WEB: 'In-App', + SMS: 'Text', + CALL: 'Automated Calling', + PUSH: 'Mobile', + WEB_PUSH: 'Browser' + }; + return labels[c]; +}; + +export const getChannelIcon = (channel: Channels): React.ReactElement => { + switch (channel) { + case Channels.EMAIL: + return ; + case Channels.SMS: + return ; + case Channels.PUSH: + return ; + case Channels.CALL: + return ; + case Channels.INAPP_WEB: + return ; + case Channels.WEB_PUSH: + return ; + default: + return ; + } +}; diff --git a/lib/components/Provider/index.tsx b/lib/components/Provider/index.tsx index 3a1918f..0c636bd 100644 --- a/lib/components/Provider/index.tsx +++ b/lib/components/Provider/index.tsx @@ -5,6 +5,7 @@ import React, { useState } from 'react'; import { api } from '../../api'; +import { Channels, DeliveryOptions } from '../Notifications/interface'; type Props = { clientId: string; @@ -22,33 +23,6 @@ interface WS_NewNotification { }; } -export const NOTIFICATION_ACTIONS = { - opened: 'opened', - clicked: 'clicked', - archived: 'archived', - replied: 'replied', - actioned1: 'actioned1', - actioned2: 'actioned2' -}; - -export enum Channels { - EMAIL = 'EMAIL', - INAPP_WEB = 'INAPP_WEB', - SMS = 'SMS', - CALL = 'CALL', - PUSH = 'PUSH', - WEB_PUSH = 'WEB_PUSH' -} - -export enum DeliveryOptions { - OFF = 'off', - INSTANT = 'instant', - HOURLY = 'hourly', - DAILY = 'daily', - WEEKLY = 'weekly', - MONTHLY = 'monthly' -} - export interface Notification { envId: string; notificationId: string; From 2175246835dbb48bf08bac4b2b6a13fe761c436c Mon Sep 17 00:00:00 2001 From: mbasadi Date: Tue, 13 Aug 2024 00:36:32 -0400 Subject: [PATCH 04/12] typeError: Type 'string | true | undefined' is not assignable to type 'boolean'. This double negation (!!) converts any value to its equivalent boolean. --- lib/components/Notifications/Notification.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/components/Notifications/Notification.tsx b/lib/components/Notifications/Notification.tsx index 53d0566..c323304 100644 --- a/lib/components/Notifications/Notification.tsx +++ b/lib/components/Notifications/Notification.tsx @@ -39,9 +39,9 @@ export const Notification = (props: { }) => { return ( { props.markAsClicked(props.notification.id); if (props.notification.redirectURL) { From 67f20ca41cb0f6a638f50157aa55a590aefedbcb Mon Sep 17 00:00:00 2001 From: mbasadi Date: Tue, 13 Aug 2024 00:38:30 -0400 Subject: [PATCH 05/12] IMPORTANT: use useCallback to prevent fetching notification on re rendering --- lib/components/Provider/index.tsx | 154 ++++++++++++++++-------------- 1 file changed, 84 insertions(+), 70 deletions(-) diff --git a/lib/components/Provider/index.tsx b/lib/components/Provider/index.tsx index 0c636bd..776341a 100644 --- a/lib/components/Provider/index.tsx +++ b/lib/components/Provider/index.tsx @@ -2,7 +2,8 @@ import React, { PropsWithChildren, createContext, useEffect, - useState + useState, + useCallback } from 'react'; import { api } from '../../api'; import { Channels, DeliveryOptions } from '../Notifications/interface'; @@ -163,80 +164,93 @@ export const NotificatinAPIProvider: React.FunctionComponent< }); }; - const getNotifications = async ( - count: number, - before: number - ): Promise => { - const res = await api( - config.apiURL, - 'GET', - `notifications/INAPP_WEB?count=${count}&before=${before}`, - props.clientId, - props.userId - ); - return (res as { notifications: InappNotification[] }).notifications; - }; - - const fetchNotificationsBeforeDate = async ( - before: string, - maxCountNeeded: number, - oldestNeeded?: string - ): Promise<{ - notifications: InappNotification[]; - couldLoadMore: boolean; - oldestReceived: string; - }> => { - let result: InappNotification[] = []; - let oldestReceived = before; - let couldLoadMore = true; - let shouldLoadMore = true; - while (shouldLoadMore) { - const notis = await getNotifications( - maxCountNeeded, - new Date(oldestReceived).getTime() - ); - const notisWithoutDuplicates = notis.filter( - (n: InappNotification) => !result.find((nn) => nn.id === n.id) + const getNotifications = useCallback( + async (count: number, before: number): Promise => { + const res = await api( + config.apiURL, + 'GET', + `notifications/INAPP_WEB?count=${count}&before=${before}`, + props.clientId, + props.userId ); - oldestReceived = notisWithoutDuplicates.reduce( - (min: string, n: InappNotification) => (min < n.date ? min : n.date), - before - ); - result = [...result, ...notisWithoutDuplicates]; - - couldLoadMore = notisWithoutDuplicates.length > 0; - shouldLoadMore = true; + return (res as { notifications: InappNotification[] }).notifications; + }, + [config.apiURL, props.clientId, props.userId] // Dependencies of getNotifications + ); - if ( - !couldLoadMore || - result.length >= maxCountNeeded || - (oldestNeeded && oldestReceived < oldestNeeded) - ) { - shouldLoadMore = false; + const fetchNotificationsBeforeDate = useCallback( + async ( + before: string, + maxCountNeeded: number, + oldestNeeded?: string + ): Promise<{ + notifications: InappNotification[]; + couldLoadMore: boolean; + oldestReceived: string; + }> => { + let result: InappNotification[] = []; + let oldestReceived = before; + let couldLoadMore = true; + let shouldLoadMore = true; + while (shouldLoadMore) { + const notis = await getNotifications( + maxCountNeeded, + new Date(oldestReceived).getTime() + ); + const notisWithoutDuplicates = notis.filter( + (n: InappNotification) => !result.find((nn) => nn.id === n.id) + ); + oldestReceived = notisWithoutDuplicates.reduce( + (min: string, n: InappNotification) => (min < n.date ? min : n.date), + before + ); + result = [...result, ...notisWithoutDuplicates]; + + couldLoadMore = notisWithoutDuplicates.length > 0; + shouldLoadMore = true; + + if ( + !couldLoadMore || + result.length >= maxCountNeeded || + (oldestNeeded && oldestReceived < oldestNeeded) + ) { + shouldLoadMore = false; + } } - } - console.log(result.length, couldLoadMore, oldestReceived); - return { - notifications: result, - couldLoadMore, - oldestReceived - }; - }; + console.log(result.length, couldLoadMore, oldestReceived); + return { + notifications: result, + couldLoadMore, + oldestReceived + }; + }, + [getNotifications] + ); - const loadNotifications = async (initial?: boolean) => { - if (!hasMore) return; - if (loadingNotifications) return; - setLoadingNotifications(true); - const res = await fetchNotificationsBeforeDate( + const loadNotifications = useCallback( + async (initial?: boolean) => { + if (!hasMore) return; + if (loadingNotifications) return; + setLoadingNotifications(true); + const res = await fetchNotificationsBeforeDate( + oldestLoaded, + initial ? config.initialLoadMaxCount : 1000, + initial ? config.initialLoadMaxAge.toISOString() : undefined + ); + setOldestLoaded(res.oldestReceived); + setHasMore(res.couldLoadMore); + addNotificationsToState(res.notifications); + setLoadingNotifications(false); + }, + [ + hasMore, + loadingNotifications, oldestLoaded, - initial ? config.initialLoadMaxCount : 1000, - initial ? config.initialLoadMaxAge.toISOString() : undefined - ); - setOldestLoaded(res.oldestReceived); - setHasMore(res.couldLoadMore); - addNotificationsToState(res.notifications); - setLoadingNotifications(false); - }; + config.initialLoadMaxCount, + config.initialLoadMaxAge, + fetchNotificationsBeforeDate + ] + ); const markAsClicked = async (id: string) => { const date = new Date().toISOString(); From 094cb84310981b6715afb26eb58641798526f811 Mon Sep 17 00:00:00 2001 From: mbasadi Date: Tue, 13 Aug 2024 00:40:31 -0400 Subject: [PATCH 06/12] create pull request actions --- .github/workflows/pull_request.yml | 31 ++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/pull_request.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..0761807 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,31 @@ +name: Pull Request CI + +on: + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Install dependencies + run: npm install + + - name: Run Prettier Check + run: npm run prettier-check + + - name: Run ESLint + run: npm run lint + + - name: Build Project + run: npm run build From 1ffb52a85eb49df6e194af469f079a0f6062ccef Mon Sep 17 00:00:00 2001 From: mbasadi Date: Tue, 13 Aug 2024 00:53:40 -0400 Subject: [PATCH 07/12] Push Pipeline --- .github/workflows/push_pipeline.yml | 70 +++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/push_pipeline.yml diff --git a/.github/workflows/push_pipeline.yml b/.github/workflows/push_pipeline.yml new file mode 100644 index 0000000..6721f71 --- /dev/null +++ b/.github/workflows/push_pipeline.yml @@ -0,0 +1,70 @@ +name: Push Pipeline + +on: + push: + branches: + - main + +jobs: + Release: + name: Test, Build & Publish + runs-on: ubuntu-latest + outputs: + releaseType: ${{ steps.publish.outputs.type }} + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.17 + + - name: Cache node_modules + id: cache-modules + uses: actions/cache@v1 + with: + path: node_modules + key: 18.17.x-${{ runner.OS }}-build-${{ hashFiles('package.json') }} + + - name: Install Dependencies + if: steps.cache-modules.outputs.cache-hit != 'true' + run: npm install + + - name: Run ESLint + run: npm run lint + + - name: Check Prettier Formatting + run: npm run prettier-check + + - name: Build Project + run: npm run build + + - name: Send Slack Notification (Pre-Publish) + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + fields: repo,message,action,took + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + if: always() # Send notifications even if the job fails or is canceled. + + - name: Publish to npm + id: publish + uses: JS-DevTools/npm-publish@v1 + with: + token: ${{ secrets.REACT_CLIENT_SDK_NPM_AUTH_TOKEN }} + env: + NODE_ENV: production + + - name: Send Slack Notification (Post-Publish) + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + fields: repo + text: Published to NPM ${{ steps.publish.outputs.old-version }} -> ${{ steps.publish.outputs.version }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + if: steps.publish.outputs.type != 'none' && always() From 4645e1d6e31ad409b2ab2f047a02d1dd32280b74 Mon Sep 17 00:00:00 2001 From: mbasadi Date: Tue, 13 Aug 2024 00:54:21 -0400 Subject: [PATCH 08/12] bump version to 0.0.25 because of the useCall --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c07b8a7..4cbf530 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "notificationapi-react-client-sdk", "private": false, - "version": "0.0.0", + "version": "0.0.25", "type": "module", "scripts": { "dev": "vite", From 06fe51844314b250213fa81fbab0c757092ed7e8 Mon Sep 17 00:00:00 2001 From: mbasadi Date: Tue, 13 Aug 2024 00:58:04 -0400 Subject: [PATCH 09/12] Add User-Agent --- lib/api.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/api.ts b/lib/api.ts index 5da4cd6..6a820e7 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -1,3 +1,5 @@ +const USER_AGENT = 'notificationapi-react-client-sdk'; +const VERSION = '0.0.25'; export const api = async ( endpoint: string, method: 'GET' | 'POST' | 'PATCH', @@ -16,7 +18,8 @@ export const api = async ( method, body: JSON.stringify(data), headers: { - Authorization: `Basic ${token}` + Authorization: `Basic ${token}`, + 'User-Agent': `${USER_AGENT}/${VERSION}` } } ); From a922a2dfd66b47a4d12d650fb72b3a3c95af8d16 Mon Sep 17 00:00:00 2001 From: mbasadi Date: Tue, 13 Aug 2024 01:02:45 -0400 Subject: [PATCH 10/12] change the secret name --- .github/workflows/push_pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push_pipeline.yml b/.github/workflows/push_pipeline.yml index 6721f71..5d0d85f 100644 --- a/.github/workflows/push_pipeline.yml +++ b/.github/workflows/push_pipeline.yml @@ -54,7 +54,7 @@ jobs: id: publish uses: JS-DevTools/npm-publish@v1 with: - token: ${{ secrets.REACT_CLIENT_SDK_NPM_AUTH_TOKEN }} + token: ${{ secrets.NPM_AUTH_TOKEN }} env: NODE_ENV: production From 696f1954e77af7002bc41933cd509937369bc32d Mon Sep 17 00:00:00 2001 From: mbasadi Date: Tue, 13 Aug 2024 12:29:27 -0400 Subject: [PATCH 11/12] node version is 20 --- .github/workflows/pull_request.yml | 2 +- .github/workflows/push_pipeline.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 0761807..55ffb89 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: '18' + node-version: '20' - name: Install dependencies run: npm install diff --git a/.github/workflows/push_pipeline.yml b/.github/workflows/push_pipeline.yml index 5d0d85f..c8d4ce2 100644 --- a/.github/workflows/push_pipeline.yml +++ b/.github/workflows/push_pipeline.yml @@ -18,7 +18,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 18.17 + node-version: 20 - name: Cache node_modules id: cache-modules From fcb33518e825f3f951754c26fd1fe875426b73cb Mon Sep 17 00:00:00 2001 From: mbasadi Date: Tue, 13 Aug 2024 17:25:10 -0400 Subject: [PATCH 12/12] add npm version --- package.json | 3 ++- scripts/version-update.sh | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 scripts/version-update.sh diff --git a/package.json b/package.json index 4cbf530..dafcd36 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "format": "prettier --write \"{src,lib}/**/*.{ts,tsx,js,jsx,json,css,md,html}\"", "prettier-check": "prettier --check \"{src,lib}/**/*.{ts,tsx,js,jsx,json,css,md,html}\"", "preview": "vite preview", - "prepublishOnly": "npm run build" + "prepublishOnly": "npm run build", + "version": "bash scripts/version-update.sh && git add lib/api.ts" }, "peerDependencies": { "react": "^18.2.0", diff --git a/scripts/version-update.sh b/scripts/version-update.sh new file mode 100644 index 0000000..ca6c64e --- /dev/null +++ b/scripts/version-update.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Get the version from package.json +VERSION=$(jq -r .version package.json) + +# Ensure VERSION is not empty +if [ -z "$VERSION" ]; then + echo "Error: Version not found in package.json" + exit 1 +fi + +# Update the VERSION in api.ts +sed -i.bak "s/\(const VERSION = '\)[^']*\(';\)/\1$VERSION\2/" lib/api.ts + +# Verify if VERSION was updated correctly +if grep -q "const VERSION = '$VERSION';" lib/api.ts; then + echo "Updated VERSION in api.ts to $VERSION" +else + echo "Failed to update VERSION in api.ts" +fi + +# Clean up the backup file created by sed +rm lib/api.ts.bak