diff --git a/.github/workflows/code_reviewer-updated.yml b/.github/workflows/code_reviewer-updated.yml new file mode 100644 index 000000000..cc270edc1 --- /dev/null +++ b/.github/workflows/code_reviewer-updated.yml @@ -0,0 +1,22 @@ +name: AI PR Reviewer Updated + +on: + pull_request: + types: + - opened + - synchronize +permissions: + pull-requests: write +jobs: + tc-ai-pr-review: + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + + - name: TC AI PR Reviewer + uses: topcoder-platform/tc-ai-pr-reviewer@prompt-update + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # The GITHUB_TOKEN is there by default so you just need to keep it like it is and not necessarily need to add it as secret as it will throw an error. [More Details](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret) + LAB45_API_KEY: ${{ secrets.LAB45_API_KEY }} + exclude: '**/*.json, **/*.md, **/*.jpg, **/*.png, **/*.jpeg, **/*.bmp, **/*.webp' # Optional: exclude patterns separated by commas diff --git a/src/apps/platform/src/PlatformApp.tsx b/src/apps/platform/src/PlatformApp.tsx index dd89390fb..e8d9480d5 100644 --- a/src/apps/platform/src/PlatformApp.tsx +++ b/src/apps/platform/src/PlatformApp.tsx @@ -1,7 +1,7 @@ import { FC } from 'react' import { toast, ToastContainer } from 'react-toastify' -import { useViewportUnitsFix } from '~/libs/shared' +import { useViewportUnitsFix, NotificationsContainer } from '~/libs/shared' import { AppFooter } from './components/app-footer' import { AppHeader } from './components/app-header' @@ -14,6 +14,7 @@ const PlatformApp: FC<{}> = () => { return ( +
diff --git a/src/apps/platform/src/providers/Providers.tsx b/src/apps/platform/src/providers/Providers.tsx index b16709bb2..b7064659d 100644 --- a/src/apps/platform/src/providers/Providers.tsx +++ b/src/apps/platform/src/providers/Providers.tsx @@ -1,7 +1,7 @@ import { FC, ReactNode } from 'react' import { authUrlLogout, ProfileProvider } from '~/libs/core' -import { ConfigContextProvider } from '~/libs/shared' +import { ConfigContextProvider, NotificationProvider } from '~/libs/shared' import { PlatformRouterProvider } from './platform-router.provider' @@ -13,7 +13,9 @@ const Providers: FC = props => ( - {props.children} + + {props.children} + diff --git a/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx b/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx index bd44eb851..066de86c4 100644 --- a/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx +++ b/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx @@ -60,6 +60,7 @@ import { } from '../../../config/routes.config' import styles from './ChallengeDetailsPage.module.scss' +import { useNotification } from '~/libs/shared' interface Props { className?: string @@ -226,6 +227,7 @@ const computePhaseCompletionFromScreenings = ( // eslint-disable-next-line complexity export const ChallengeDetailsPage: FC = (props: Props) => { + const { showBannerNotification, removeNotification } = useNotification(); const [searchParams, setSearchParams] = useSearchParams() const location = useLocation() const navigate = useNavigate() @@ -1323,6 +1325,14 @@ export const ChallengeDetailsPage: FC = (props: Props) => { : undefined const shouldShowChallengeMetaRow = Boolean(statusLabel) || trackTypePills.length > 0 + useEffect(() => { + const notification = showBannerNotification({ + id: 'ai-review-scores-warning', + message: 'AI Review Scores are advisory only to provide immediate, educational, and actionable feedback to members. AI Review Scores are not influence winner selection.', + }) + return () => notification && removeNotification(notification.id); + }, [showBannerNotification]); + return ( = createContext(defaultProfileContextData) +export const useProfileContext = () => useContext(profileContext); + export default profileContext diff --git a/src/libs/shared/lib/components/index.ts b/src/libs/shared/lib/components/index.ts index ac827849a..8b9d29e69 100644 --- a/src/libs/shared/lib/components/index.ts +++ b/src/libs/shared/lib/components/index.ts @@ -3,6 +3,7 @@ export * from './modals' export * from './profile-picture' export * from './input-skill-selector' export * from './member-skill-editor' +export * from './notifications' export * from './skill-pill' export * from './expandable-list' export * from './grouped-skills-ui' diff --git a/src/libs/shared/lib/components/notifications/Notifications.container.tsx b/src/libs/shared/lib/components/notifications/Notifications.container.tsx new file mode 100644 index 000000000..1e809dcfd --- /dev/null +++ b/src/libs/shared/lib/components/notifications/Notifications.container.tsx @@ -0,0 +1,19 @@ +import { FC } from 'react' + +import { Notification } from '~/libs/ui' + +import { useNotification } from './Notifications.context'; + +const NotificationsContainer: FC = () => { + const { notifications, removeNotification } = useNotification(); + + return ( +
+ {notifications.map(n => ( + + ))} +
+ ) +} + +export default NotificationsContainer diff --git a/src/libs/shared/lib/components/notifications/Notifications.context.tsx b/src/libs/shared/lib/components/notifications/Notifications.context.tsx new file mode 100644 index 000000000..323ec6972 --- /dev/null +++ b/src/libs/shared/lib/components/notifications/Notifications.context.tsx @@ -0,0 +1,74 @@ +import React, { createContext, useContext, useState, useCallback, ReactNode } from "react"; +import { useProfileContext } from "~/libs/core"; +import { dismiss, wasDismissed } from "./localstorage.utils"; + +export type NotificationType = "success" | "error" | "info" | "warning" | "banner"; + +export interface Notification { + id: string; + type: NotificationType; + message: string; + duration?: number; // in ms +} + +type NotifyPayload = string | (Partial & { message: string }) + +interface NotificationContextType { + notifications: Notification[]; + notify: (message: NotifyPayload, type?: NotificationType, duration?: number) => Notification | void; + showBannerNotification: (message: NotifyPayload) => Notification | void; + removeNotification: (id: string) => void; +} + +const NotificationContext = createContext(undefined); + +export const useNotification = (): NotificationContextType => { + const context = useContext(NotificationContext); + if (!context) throw new Error("useNotification must be used within a NotificationProvider"); + return context; +}; + +export const NotificationProvider: React.FC<{ + children: ReactNode, +}> = ({ children }) => { + const profileCtx = useProfileContext() + const uuid = profileCtx.profile?.userId ?? 'annon'; + const [notifications, setNotifications] = useState([]); + + const removeNotification = useCallback((id: string, persist?: boolean) => { + setNotifications(prev => prev.filter(n => n.id !== id)); + if (persist) { + dismiss(id); + } + }, []); + + const notify = useCallback( + (message: NotifyPayload, type: NotificationType = "info", duration = 3000) => { + const id = `${uuid}[${typeof message === 'string' ? message : message.id}]`; + const newNotification: Notification = typeof message === 'string' ? { id, message, type, duration } : { type, duration, ...message, id }; + + if (wasDismissed(id)) { + return; + } + + setNotifications(prev => [...prev, newNotification]); + + if (duration > 0) { + setTimeout(() => removeNotification(id), duration); + } + + return newNotification; + }, + [uuid] + ); + + const showBannerNotification = useCallback(( + message: NotifyPayload, + ) => notify(message, 'banner', 0), [notify]); + + return ( + + {children} + + ); +}; diff --git a/src/libs/shared/lib/components/notifications/index.ts b/src/libs/shared/lib/components/notifications/index.ts new file mode 100644 index 000000000..d2eaff448 --- /dev/null +++ b/src/libs/shared/lib/components/notifications/index.ts @@ -0,0 +1,2 @@ +export { default as NotificationsContainer } from './Notifications.container' +export * from './Notifications.context' diff --git a/src/libs/shared/lib/components/notifications/localstorage.utils.ts b/src/libs/shared/lib/components/notifications/localstorage.utils.ts new file mode 100644 index 000000000..46e776cb8 --- /dev/null +++ b/src/libs/shared/lib/components/notifications/localstorage.utils.ts @@ -0,0 +1,7 @@ +export const wasDismissed = (id: string): boolean => ( + (localStorage.getItem(`dismissed[${id}]`)) !== null +) + +export const dismiss = (id: string): void => { + localStorage.setItem(`dismissed[${id}]`, JSON.stringify(true)) +} diff --git a/src/libs/ui/lib/components/index.ts b/src/libs/ui/lib/components/index.ts index 306c469d6..2e0d2f067 100644 --- a/src/libs/ui/lib/components/index.ts +++ b/src/libs/ui/lib/components/index.ts @@ -3,6 +3,7 @@ export * from './content-layout' export * from './default-member-icon' // NOTE: for some reason, modals needs to be imported prior to form export * from './modals' +export * from './notification' export * from './form' export * from './loading-spinner' export * from './page-divider' diff --git a/src/libs/ui/lib/components/notification/Notification.tsx b/src/libs/ui/lib/components/notification/Notification.tsx new file mode 100644 index 000000000..712d3a83c --- /dev/null +++ b/src/libs/ui/lib/components/notification/Notification.tsx @@ -0,0 +1,19 @@ +import { FC } from 'react' + +import { NotificationBanner } from './banner' + +interface NotificationProps { + notification: { message: string; id: string; type: string } + onClose: (id: string, save?: boolean) => void +} + +const Notification: FC = props => { + + if (props.notification.type === 'banner') { + return props.onClose(props.notification.id, save)} /> + } + + return null; +} + +export default Notification diff --git a/src/libs/ui/lib/components/notification/banner/NotificationBanner.module.scss b/src/libs/ui/lib/components/notification/banner/NotificationBanner.module.scss new file mode 100644 index 000000000..3fa146f8f --- /dev/null +++ b/src/libs/ui/lib/components/notification/banner/NotificationBanner.module.scss @@ -0,0 +1,26 @@ +@import '../../../styles/includes'; + +.wrap { + background: #60267D; + color: $tc-white; + + .inner { + max-width: $xxl-min; + padding: $sp-3 0; + @include pagePaddings; + margin: 0 auto; + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + @include ltemd { + display: block; + position: relative; + } + } +} + +.close { + cursor: pointer; + color: $tc-white; +} diff --git a/src/libs/ui/lib/components/notification/banner/NotificationBanner.stories.tsx b/src/libs/ui/lib/components/notification/banner/NotificationBanner.stories.tsx new file mode 100644 index 000000000..4560fe417 --- /dev/null +++ b/src/libs/ui/lib/components/notification/banner/NotificationBanner.stories.tsx @@ -0,0 +1,33 @@ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable camelcase */ + +import { Meta, StoryObj } from '@storybook/react' + +import NotificationBanner from './NotificationBanner' + +const meta: Meta = { + argTypes: { + persistent: { + defaultValue: false, + description: 'Set to true to allow clicks inside the tooltip', + }, + content: { + description: 'Content displayed inside the tooltip', + }, + }, + component: NotificationBanner, + excludeStories: /.*Decorator$/, + tags: ['autodocs'], + title: 'Components/NotificationBanner', +} + +export default meta + +type Story = StoryObj; + +export const Primary: Story = { + args: { + // children: , + content: 'Help tooltip', + }, +} diff --git a/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx b/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx new file mode 100644 index 000000000..04dfd28b9 --- /dev/null +++ b/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx @@ -0,0 +1,36 @@ +import { FC, ReactNode, useCallback } from 'react' + +import styles from './NotificationBanner.module.scss' +import { InformationCircleIcon, XCircleIcon } from '@heroicons/react/outline' + +interface NotificationBannerProps { + persistent?: boolean + content: ReactNode + icon?: ReactNode + onClose?: (save?: boolean) => void +} + +const NotificationBanner: FC = props => { + + return ( +
+
+ {props.icon || ( +
+ +
+ )} + + {props.content} + + {!props.persistent && ( +
props.onClose?.(true)}> + +
+ )} +
+
+ ) +} + +export default NotificationBanner diff --git a/src/libs/ui/lib/components/notification/banner/index.ts b/src/libs/ui/lib/components/notification/banner/index.ts new file mode 100644 index 000000000..51f3cf392 --- /dev/null +++ b/src/libs/ui/lib/components/notification/banner/index.ts @@ -0,0 +1 @@ +export { default as NotificationBanner } from './NotificationBanner' diff --git a/src/libs/ui/lib/components/notification/index.ts b/src/libs/ui/lib/components/notification/index.ts new file mode 100644 index 000000000..ef0ca420e --- /dev/null +++ b/src/libs/ui/lib/components/notification/index.ts @@ -0,0 +1,2 @@ +export * from './banner' +export { default as Notification } from './Notification'