From 8d9e76faeec8c0da02409dc45ecc128f2dd4e2e8 Mon Sep 17 00:00:00 2001 From: zuies Date: Thu, 18 Jan 2024 19:25:29 +0300 Subject: [PATCH 01/30] fix issue on fetch data first render --- src/components/pages/pageIndex/HeatmapChart.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/pages/pageIndex/HeatmapChart.tsx b/src/components/pages/pageIndex/HeatmapChart.tsx index b81366eb..d578ea17 100644 --- a/src/components/pages/pageIndex/HeatmapChart.tsx +++ b/src/components/pages/pageIndex/HeatmapChart.tsx @@ -37,6 +37,7 @@ const HeatmapChart = () => { const [heatmapChartOptions, setHeatmapChartOptions] = useState( defaultHeatmapChartOptions ); + const [platformFetched, setPlatformFetched] = useState(false); const defaultEndDate = moment().subtract(1, 'day'); const defaultStartDate = moment(defaultEndDate).subtract(7, 'days'); @@ -93,8 +94,11 @@ const HeatmapChart = () => { }; useEffect(() => { - fetchPlatformChannels(); - fetchData(); + const initializeSelectedChannels = async () => { + await fetchPlatformChannels(); + }; + + initializeSelectedChannels(); }, []); const handleSelectedZone = (zone: string) => { @@ -144,7 +148,9 @@ const HeatmapChart = () => { }; const handleFetchHeatmapByChannels = () => { - fetchData(); + if (platformFetched) { + fetchData(); + } }; const fetchPlatformChannels = async () => { @@ -158,6 +164,7 @@ const HeatmapChart = () => { } else { await refreshData(platformId); } + setPlatformFetched(true); } } catch (error) { } finally { @@ -169,7 +176,7 @@ const HeatmapChart = () => { return; } fetchData(); - }, [dateRange, selectedZone, platformId]); + }, [dateRange, selectedZone, platformId, platformFetched]); return (
From 33d3cfaf94964733b7896edf7ba4dc5bcfc8a76d Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 22 Dec 2023 17:46:06 +0300 Subject: [PATCH 02/30] remove useless code on privateRoute.tsx --- src/utils/privateRoute.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/utils/privateRoute.tsx b/src/utils/privateRoute.tsx index eddcb90a..6af49fcf 100644 --- a/src/utils/privateRoute.tsx +++ b/src/utils/privateRoute.tsx @@ -17,9 +17,6 @@ export default function PrivateRoute({ [router.pathname] ); - const isObjectNotEmpty = (obj: Record): boolean => { - return Object.keys(obj).length > 0; - }; useEffect(() => { if (!isCentricRoute) { const storedToken = StorageService.readLocalStorage('user'); From ea3d5c486d79f828710e16a3606e9d25ff243d7d Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 22 Dec 2023 17:48:57 +0300 Subject: [PATCH 03/30] remove useless code on index.tsx route and remove old components --- .../pages/pageIndex/FooterSection.tsx | 138 ------------------ .../pages/pageIndex/HeaderSection.tsx | 33 ----- src/pages/index.tsx | 11 -- 3 files changed, 182 deletions(-) delete mode 100644 src/components/pages/pageIndex/FooterSection.tsx delete mode 100644 src/components/pages/pageIndex/HeaderSection.tsx diff --git a/src/components/pages/pageIndex/FooterSection.tsx b/src/components/pages/pageIndex/FooterSection.tsx deleted file mode 100644 index be4b8392..00000000 --- a/src/components/pages/pageIndex/FooterSection.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import Image from 'next/image'; - -import graph from '../../../assets/svg/graph.svg'; -import members from '../../../assets/svg/members.svg'; -import metrics from '../../../assets/svg/metrics.svg'; -import arrowBottom from '../../../assets/svg/arrowBottom.svg'; -import benchmark from '../../../assets/svg/benchmark.svg'; - -import { BsClockHistory } from 'react-icons/bs'; -import { HiOutlineArrowRight } from 'react-icons/hi'; -import Link from 'next/link'; - -export const FooterSection = (): JSX.Element => { - return ( - <> -
-
-
-

- Spot value-adding members in your community -

-
- Image Alt -
-
-
-

- Use data to improve onboarding -

-
- Image Alt -
-
-
-
-

- Explore all the metrics that determine
the health of your - community -

-
- Picture of the author -
- Read our research on - -
-
-
-

- Monitor members who disengage and take action to bring them back -

-
- Image Alt -
-
-
-

- Benchmark your metrics and learn from others -

-
- Image Alt -
-
-
-
- - ); -}; diff --git a/src/components/pages/pageIndex/HeaderSection.tsx b/src/components/pages/pageIndex/HeaderSection.tsx deleted file mode 100644 index c0620b51..00000000 --- a/src/components/pages/pageIndex/HeaderSection.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { ImArrowDown } from 'react-icons/im'; - -export const HeaderSection = (): JSX.Element => { - return ( - <> -
-
-
-

- The new way to manage your community -

-
-
-

- We believe communities are the beating heart of DAOs. But there - was no way to assess and improve. We assembled a team of - scientists to empower you with deep, actionable insights. -

-

- And while the team is busy building a suite of tools, below is a - - small appetizer to get you started - - -

-
-
-
- - ); -}; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 6bcb0683..3aae153d 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,7 +1,5 @@ import { defaultLayout } from '../layouts/defaultLayout'; import SEO from '../components/global/SEO'; -import { useState } from 'react'; -import { StorageService } from '../services/StorageService'; import EmptyState from '../components/global/EmptyState'; import Image from 'next/image'; import emptyState from '../assets/svg/empty-state.svg'; @@ -13,17 +11,8 @@ import { ChannelProvider } from '../context/ChannelContext'; import { useToken } from '../context/TokenContext'; function Dashboard(): JSX.Element { - const [alertStateOpen, setAlertStateOpen] = useState(false); const { community } = useToken(); - const toggleAnalysisState = () => { - StorageService.writeLocalStorage('analysis_state', { - isRead: true, - visible: false, - }); - setAlertStateOpen(false); - }; - if (!community || community?.platforms?.length === 0) { return ( <> From f7d3b07515173337752f598f87cffdfad140e454 Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 22 Dec 2023 17:51:28 +0300 Subject: [PATCH 04/30] remove old settings components --- .../pages/settings/ChannelSelection.tsx | 400 ------------------ .../settings/ConfirmStartProcessing.spec.tsx | 85 ---- .../pages/settings/ConfirmStartProcessing.tsx | 75 ---- .../pages/settings/ConnectCommunities.tsx | 260 ------------ .../settings/ConnectedCommunitiesItem.tsx | 91 ---- .../settings/ConnectedCommunitiesList.tsx | 159 ------- .../pages/settings/ConnectedTwitter.tsx | 80 ---- .../pages/settings/DataAnalysis.tsx | 171 -------- .../pages/settings/IntegrateDiscord.tsx | 14 - 9 files changed, 1335 deletions(-) delete mode 100644 src/components/pages/settings/ChannelSelection.tsx delete mode 100644 src/components/pages/settings/ConfirmStartProcessing.spec.tsx delete mode 100644 src/components/pages/settings/ConfirmStartProcessing.tsx delete mode 100644 src/components/pages/settings/ConnectCommunities.tsx delete mode 100644 src/components/pages/settings/ConnectedCommunitiesItem.tsx delete mode 100644 src/components/pages/settings/ConnectedCommunitiesList.tsx delete mode 100644 src/components/pages/settings/ConnectedTwitter.tsx delete mode 100644 src/components/pages/settings/DataAnalysis.tsx delete mode 100644 src/components/pages/settings/IntegrateDiscord.tsx diff --git a/src/components/pages/settings/ChannelSelection.tsx b/src/components/pages/settings/ChannelSelection.tsx deleted file mode 100644 index fdd03676..00000000 --- a/src/components/pages/settings/ChannelSelection.tsx +++ /dev/null @@ -1,400 +0,0 @@ -import { - Accordion, - AccordionDetails, - AccordionSummary, - Dialog, -} from '@mui/material'; -import React, { useEffect, useState } from 'react'; -import { IoClose } from 'react-icons/io5'; -import useAppStore from '../../../store/useStore'; -import ChannelList from '../login/ChannelList'; -import { StorageService } from '../../../services/StorageService'; -import { - IGuild, - IGuildChannels, - IUser, - ISubChannels, - IChannelWithoutId, -} from '../../../utils/types'; -import { BiError } from 'react-icons/bi'; -import CustomButton from '../../global/CustomButton'; -import { FiRefreshCcw } from 'react-icons/fi'; -import Loading from '../../global/Loading'; -import { MdExpandMore } from 'react-icons/md'; -import ConfirmStartProcessing from './ConfirmStartProcessing'; -import clsx from 'clsx'; - -type IProps = { - emitable?: boolean; - submit?: (selectedChannels: IChannelWithoutId[]) => unknown; -}; -export default function ChannelSelection({ emitable, submit }: IProps) { - const [open, setOpen] = useState(false); - const [openProcessing, SetOpenProcessing] = useState(false); - - const [fullWidth, setFullWidth] = React.useState(true); - const [guild, setGuild] = useState(); - const [channels, setChannels] = useState>([]); - const [selectedChannels, setSelectedChannels] = useState< - Array - >([]); - - const { - guildChannels, - guildInfo, - updateSelectedChannels, - getUserGuildInfo, - guilds, - isRefetchLoading, - refetchGuildChannels, - } = useAppStore(); - - useEffect(() => { - const user = StorageService.readLocalStorage('user'); - if (user) { - setGuild(user.guild); - } - - const activeChannles = - guildInfo && guildInfo.selectedChannels - ? guildInfo.selectedChannels.map( - (channel: { - channelId: string; - channelName: string; - _id: string; - }) => { - return channel.channelId; - } - ) - : []; - - const channels = guildChannels.map( - (guild: IGuildChannels, _index: number) => { - const selected: Record = {}; - - guild.subChannels.forEach((subChannel: ISubChannels) => { - if (activeChannles.includes(subChannel.channelId)) { - selected[subChannel.channelId] = true; - } else { - selected[subChannel.channelId] = false; - } - }); - - return { ...guild, selected: selected }; - } - ); - - const subChannelsStatus = channels.map((channel: IGuildChannels) => { - return channel.selected; - }); - - const selectedChannelsStatus = Object.assign({}, ...subChannelsStatus); - let activeChannel: string[] = []; - for (const key in selectedChannelsStatus) { - if (selectedChannelsStatus[key]) { - activeChannel.push(key); - } - } - - const result = ([] as IChannelWithoutId[]).concat( - ...channels.map((channel: IGuildChannels) => { - return channel.subChannels - .filter((subChannel: ISubChannels) => { - if (activeChannel.includes(subChannel.channelId)) { - return subChannel; - } - }) - .map((filterdItem: ISubChannels) => { - return { - channelId: filterdItem.channelId, - channelName: filterdItem.name, - }; - }); - }) - ); - setSelectedChannels(result); - setChannels(channels); - }, [guildChannels]); - - const onChange = ( - channelId: string, - subChannelId: string, - status: boolean - ) => { - setChannels((preChannels) => { - return preChannels.map((preChannel) => { - if (preChannel.channelId !== channelId) return preChannel; - - const selected = preChannel.selected ?? {}; - selected[subChannelId] = status; - - return { ...preChannel, selected }; - }); - }); - }; - const handleCheckAll = (guild: IGuildChannels, status: boolean) => { - const selectedGuild = channels.find( - (channel) => channel.channelId === guild.channelId - ); - if (!selectedGuild) return; - - const updatedChannels = channels.map((channel: IGuildChannels) => { - if (channel === selectedGuild) { - const selected = { ...channel.selected }; - Object.keys(selected).forEach((key) => (selected[key] = status)); - return { ...channel, selected }; - } - return channel; - }); - - setChannels(updatedChannels); - }; - - const refetchChannels = () => { - refetchGuildChannels(guild?.guildId); - }; - - const submitChannels = () => { - const subChannelsStatus = channels.map((channel: IGuildChannels) => { - return channel.selected; - }); - - const selectedChannelsStatus = Object.assign({}, ...subChannelsStatus); - let activeChannel: string[] = []; - for (const key in selectedChannelsStatus) { - if (selectedChannelsStatus[key]) { - activeChannel.push(key); - } - } - - const result = ([] as IChannelWithoutId[]).concat( - ...channels.map((channel: IGuildChannels) => { - return channel.subChannels - .filter((subChannel: ISubChannels) => { - if ( - activeChannel.includes(subChannel.channelId) && - subChannel.canReadMessageHistoryAndViewChannel - ) { - return subChannel; - } - }) - .map((filterdItem: ISubChannels) => { - return { - channelId: filterdItem.channelId, - channelName: filterdItem.name, - }; - }); - }) - ); - - setSelectedChannels(result); - if (emitable) { - if (submit) submit(result); - setOpen(false); - } else { - setOpen(false); - SetOpenProcessing(true); - } - }; - - const handleClose = () => { - setOpen(false); - }; - - const handleCloseProcessingModal = () => { - SetOpenProcessing(false); - }; - const handleToProcess = () => { - updateSelectedChannels(guild?.guildId, selectedChannels).then( - (_res: unknown) => { - SetOpenProcessing(false); - getUserGuildInfo(guild?.guildId); - } - ); - }; - - if (guilds.length === 0) { - return ( -
- Selected channels: {selectedChannels.length}{' '} - setOpen(true)} - > - Show Channels - -

- - - There is no community connected at the moment. To be able to change - channels, please
connect your community first. -
-

-
- ); - } - - return ( -
-

- Selected channels:{' '} - - {guildInfo && guildInfo.isDisconnected ? 0 : selectedChannels.length} - {' '} - setOpen(true)} - > - Show Channels - -

- {guildInfo && guildInfo.isInProgress ? ( -
- -

- We are processing data from selected channels. It might take up to 6 - hours to complete. -

-
- ) : ( - '' - )} - -
-
-
-

- Import activities from channels -

- -
-

- Select channels to import activity in this workspace. Please give - Together Crew access to all selected private channels by updating - the channels permissions in Discord. Discord permission will - affect the channels the bot can see. -

-
-
- {isRefetchLoading ? ( - - ) : ( -
-
- } - size="large" - variant="outlined" - onClick={refetchChannels} - /> -
- {channels && channels.length > 0 - ? channels.map((guild: IGuildChannels, index: number) => { - return ( -
- -
- ); - }) - : ''} -
- )} -
- - - } - > -

- How to give access to the channel you want to import? -

-
- -
-
    -
  1. - Navigate to the channel you want to import on{' '} - - Discord - -
  2. -
  3. - Go to the settings for that specific channel (select the - wheel on the right of the channel name) -
  4. -
  5. - Select Permissions (left sidebar), and then in the - middle of the screen check Advanced permissions -
  6. -
  7. - With the TogetherCrew Bot selected, under Advanced - Permissions, make sure that [View channel] and [Read message - history] are marked as [✓] -
  8. -
  9. - Select the plus sign to the right of Roles/Members and under - members select TogetherCrew bot -
  10. -
  11. - Click on the Refresh List button on this window and - select the new channels -
  12. -
-
-
-
-
- -
-
-
- {' '} -
- ); -} - -ChannelSelection.defaultProps = { - emitable: false, -}; diff --git a/src/components/pages/settings/ConfirmStartProcessing.spec.tsx b/src/components/pages/settings/ConfirmStartProcessing.spec.tsx deleted file mode 100644 index 7d6ed312..00000000 --- a/src/components/pages/settings/ConfirmStartProcessing.spec.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import ConfirmStartProcessing from './ConfirmStartProcessing'; - -describe('ConfirmStartProcessing', () => { - const onClose = jest.fn(); - const onSubmitProcess = jest.fn(); - - beforeEach(() => { - onClose.mockClear(); - onSubmitProcess.mockClear(); - }); - - it('renders the correct text', () => { - render( - - ); - - expect( - screen.getByText( - /Data from selected channels may take some time to process/i - ) - ).toBeInTheDocument(); - expect( - screen.getByText( - /Please confirm you want to start data processing. It might take up to 6 hours to complete. Once it is done we will send you a message on Discord./i - ) - ).toBeInTheDocument(); - expect( - screen.getByText( - /During this period, it will not be possible to change your imported channels./i - ) - ).toBeInTheDocument(); - expect(screen.getByText(/Cancel/i)).toBeInTheDocument(); - expect(screen.getByText('Start data processing')).toBeInTheDocument(); - }); - - it('calls onClose when cancel button is clicked', () => { - render( - - ); - - const cancelButton = screen.getByText(/Cancel/i); - fireEvent.click(cancelButton); - - expect(onClose).toHaveBeenCalledTimes(1); - }); - - it('calls onSubmitProcess when start button is clicked', () => { - render( - - ); - - const startButton = screen.getByText('Start data processing'); - fireEvent.click(startButton); - - expect(onSubmitProcess).toHaveBeenCalledTimes(1); - }); - - it('calls onClose when close icon is clicked', () => { - render( - - ); - - const closeIcon = screen.getByTestId('close-modal-icon'); - fireEvent.click(closeIcon); - - expect(onClose).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/components/pages/settings/ConfirmStartProcessing.tsx b/src/components/pages/settings/ConfirmStartProcessing.tsx deleted file mode 100644 index 2e952cb4..00000000 --- a/src/components/pages/settings/ConfirmStartProcessing.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Dialog, DialogTitle } from '@mui/material'; -import React from 'react'; -import { BsClockHistory } from 'react-icons/bs'; -import CustomButton from '../../global/CustomButton'; -import { IoClose } from 'react-icons/io5'; - -interface ConfirmStartProcessingProps { - open: boolean; - onClose: () => void; - onSubmitProcess: () => void; -} - -function ConfirmStartProcessing(props: ConfirmStartProcessingProps) { - const { open, onClose, onSubmitProcess } = props; - return ( - - - - -
- -

- Data from selected channels may take some time to process -

-

- Please confirm you want to start data processing. It might take up to - 6 hours to complete. Once it is done we will send you a message on - Discord. -

-

- During this period, it will not be possible to change your imported - channels. -

-
- - -
-
-
- ); -} - -export default ConfirmStartProcessing; diff --git a/src/components/pages/settings/ConnectCommunities.tsx b/src/components/pages/settings/ConnectCommunities.tsx deleted file mode 100644 index 19a3f4b3..00000000 --- a/src/components/pages/settings/ConnectCommunities.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import { Paper, Tooltip, Typography } from '@mui/material'; -import { useEffect, useState } from 'react'; -import { FaDiscord } from 'react-icons/fa'; -import { GoPlus } from 'react-icons/go'; -import CustomButton from '../../global/CustomButton'; -import DatePeriodRange from '../../global/DatePeriodRange'; -import CustomModal from '../../global/CustomModal'; -import ChannelSelection from './ChannelSelection'; -import { BsClockHistory, BsTwitter } from 'react-icons/bs'; -import useAppStore from '../../../store/useStore'; -import { useRouter } from 'next/router'; -import moment from 'moment'; -import { StorageService } from '../../../services/StorageService'; -import { IUser } from '../../../utils/types'; - -import { - setAmplitudeUserIdFromToken, - trackAmplitudeEvent, -} from '../../../helpers/amplitudeHelper'; -import { decodeUserTokenDiscordId } from '../../../helpers/helper'; - -export default function ConnectCommunities() { - const router = useRouter(); - - const user = StorageService.readLocalStorage('user'); - - const [open, setOpen] = useState(false); - const [confirmModalOpen, setConfirmModalOpen] = useState(false); - const [guildId, setGuildId] = useState(''); - const [activePeriod, setActivePeriod] = useState(1); - const [datePeriod, setDatePeriod] = useState(''); - const [selectedChannels, setSelectedChannels] = useState([]); - - const { - guilds, - connectNewGuild, - patchGuildById, - getUserGuildInfo, - authorizeTwitter, - } = useAppStore(); - - if (typeof window !== 'undefined') { - useEffect(() => { - if (Object.keys(router?.query).length > 0 && router.query.isSuccessful) { - const { guildId, guildName } = router?.query; - let user: any = StorageService.readLocalStorage('user'); - user = { token: user.token, guild: { guildId, guildName } }; - StorageService.writeLocalStorage('user', user); - setGuildId(guildId); - toggleModal(true); - setDatePeriod( - moment().subtract('35', 'days').format('YYYY-MM-DDTHH:mm:ss[Z]') - ); - } - }, [router]); - } - - const updateSelectedChannels = (channels: any) => { - setSelectedChannels(channels); - }; - - const handleActivePeriod = (dateRangeType: number | string) => { - let dateTime = ''; - switch (dateRangeType) { - case 1: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('35', 'days') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - case 2: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('3', 'months') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - case 3: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('6', 'months') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - case 4: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('1', 'year') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - default: - break; - } - setDatePeriod(dateTime); - }; - - const submitGuild = async () => { - await patchGuildById(guildId, datePeriod, selectedChannels).then( - (_res: any) => { - setOpen(false); - toggleConfirmModal(true); - } - ); - }; - - const toggleModal = (e: boolean) => { - setOpen(e); - }; - - const toggleConfirmModal = (e: boolean) => { - setConfirmModalOpen(e); - router.replace({ - pathname: '/settings', - }); - }; - - const handleConnectedGuild = () => { - const user: IUser | undefined = - StorageService.readLocalStorage('user'); - - setAmplitudeUserIdFromToken(); - - trackAmplitudeEvent({ - eventType: 'update_connected_guild_on_settings', - eventProperties: { - guild: user?.guild, - }, - }); - getUserGuildInfo(guildId); - setConfirmModalOpen(false); - }; - - const handleAuthorizeTwitter = () => { - authorizeTwitter(decodeUserTokenDiscordId(user)); - }; - const isAllTwitterPropertiesNull = - user && - user.twitter && - Object.values(user.twitter).every((value) => value == null); - - return ( - <> - -
- -

{"Perfect, you're all set!"}

-

- Data import just started. It might take up to 6 hours to finish. - Once it is done we will send you a message on Discord. -

- { - handleConnectedGuild(); - }} - /> -
-
- -
-

- Choose date period for data analysis -

-

- You will be able to change date period and selected channels in the - future. -

- -
-
-

- Confirm your imported channels -

- updateSelectedChannels(channels)} - /> -
- { - submitGuild(); - }} - /> -
-
-
-
-
-

- Connect your communities -

-
- {isAllTwitterPropertiesNull ? ( -
- handleAuthorizeTwitter()} - > -

Twitter

- -
- -

Connect

-
-
-
- ) : ( - <> - )} -
- {guilds.length >= 1 ? ( - - It will be possible to connect more communities soon. - - } - arrow - placement="right" - > - -

Discord

- -
- -

Connect

-
-
-
- ) : ( - connectNewGuild()} - > -

Discord

- -
- -

Connect

-
-
- )} -
-
-
-
- - ); -} diff --git a/src/components/pages/settings/ConnectedCommunitiesItem.tsx b/src/components/pages/settings/ConnectedCommunitiesItem.tsx deleted file mode 100644 index 787156e5..00000000 --- a/src/components/pages/settings/ConnectedCommunitiesItem.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { Paper, Tooltip, Typography } from '@mui/material'; -import { FaDiscord } from 'react-icons/fa'; -import Image from 'next/image'; -import moment from 'moment'; - -type IProps = { - guild: any; - onClick: (guildId: string) => void; -}; -export default function ConnectedCommunitiesItem({ guild, onClick }: IProps) { - return ( -
- -
-
-

Discord

- {!guild.isInProgress || guild.isDisconnected ? ( - - {guild.isDisconnected - ? 'We don’t have access to your server anymore. Please make sure the Bot is installed properly.' - : !guild.isInProgress - ? 'Discord is connected' - : 'The Discord bot has been connected, and we need time to analyze your data'} - - } - arrow - placement="right" - > - - - ) : ( - - )} -
- -
-
- {guild.guildId && guild.icon ? ( - {guild.name - ) : ( -
- )} -
-

{guild.name}

-

- {!guild.isInProgress || guild.isDisconnected - ? `Connected ${moment(guild.connectedAt).format('DD MMM yyyy')}` - : 'Data import in progress'} -

-
-
-
onClick(guild.guildId)} - > - Disconnect -
- -
- ); -} diff --git a/src/components/pages/settings/ConnectedCommunitiesList.tsx b/src/components/pages/settings/ConnectedCommunitiesList.tsx deleted file mode 100644 index f1972bb2..00000000 --- a/src/components/pages/settings/ConnectedCommunitiesList.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { useState } from 'react'; -import CustomModal from '../../global/CustomModal'; -import CustomButton from '../../global/CustomButton'; -import ConnectedCommunitiesItem from './ConnectedCommunitiesItem'; -import { toast } from 'react-toastify'; -import { FaRegCheckCircle } from 'react-icons/fa'; -import { Paper } from '@mui/material'; -import useAppStore from '../../../store/useStore'; -import { DISCONNECT_TYPE } from '../../../store/types/ISetting'; -import { StorageService } from '../../../services/StorageService'; -import { ITwitter, IUser } from '../../../utils/types'; - -import { - setAmplitudeUserIdFromToken, - trackAmplitudeEvent, -} from '../../../helpers/amplitudeHelper'; -import ConnectedTwitter from './ConnectedTwitter'; - -export default function ConnectedCommunitiesList({ guilds }: any) { - const { disconnecGuildById, getGuilds } = useAppStore(); - const [open, setOpen] = useState(false); - const [guildId, setGuildId] = useState(''); - const toggleModal = (e: boolean) => { - setOpen(e); - }; - let user: IUser | undefined = StorageService.readLocalStorage('user'); - const notify = () => { - toast('The integration has been disconnected succesfully.', { - position: 'top-center', - autoClose: 3000, - hideProgressBar: true, - closeOnClick: false, - pauseOnHover: true, - draggable: false, - progress: undefined, - closeButton: false, - theme: 'light', - icon: , - }); - }; - - const disconnectGuild = (discconectType: DISCONNECT_TYPE) => { - disconnecGuildById(guildId, discconectType).then((_res: any) => { - notify(); - getGuilds(); - - setAmplitudeUserIdFromToken(); - - trackAmplitudeEvent({ - eventType: 'disconnect_guild_on_setting', - eventProperties: { - guild: user?.guild, - }, - }); - - if (user) { - user = { token: user.token, guild: { guildId: '', guildName: '' } }; - StorageService.writeLocalStorage('user', user); - } - }); - }; - - function isAllTwitterPropertiesNull(twitter: ITwitter): boolean { - return ( - twitter.twitterConnectedAt === null && - twitter.twitterId === null && - twitter.twitterProfileImageUrl === null && - twitter.twitterUsername === null - ); - } - - return ( - <> - {guilds && guilds.length > 0 ? ( -
-

Connected communities

-
- {guilds && guilds.length > 0 - ? guilds.map((guild: any) => ( -
- { - setGuildId(guildId), setOpen(true); - }} - /> -
- )) - : ''} - {user?.twitter && !isAllTwitterPropertiesNull(user.twitter) ? ( -
- -
- ) : ( - <> - )} -
-
- ) : ( - '' - )} - -
-

- Are you sure you want to disconnect{' '} -
your community? -

-
- -
-

Disconnect and delete data

-

- Importing activities and members will be stopped. Historical - activities will be deleted. -

-
- { - disconnectGuild('hard'); - }} - /> -
- -
-

Disconnect only

-

- Importing activities and members will be stopped. Historical - activities will not be affected. -

-
- { - disconnectGuild('soft'); - }} - /> -
-
-
-
- - ); -} diff --git a/src/components/pages/settings/ConnectedTwitter.tsx b/src/components/pages/settings/ConnectedTwitter.tsx deleted file mode 100644 index de5be7b0..00000000 --- a/src/components/pages/settings/ConnectedTwitter.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Avatar, Paper } from '@mui/material'; -import React from 'react'; -import { ITwitter } from '../../../utils/types'; -import useAppStore from '../../../store/useStore'; -import { BsTwitter } from 'react-icons/bs'; -import moment from 'moment'; -import { StorageService } from '../../../services/StorageService'; -import clsx from 'clsx'; - -interface IConnectedTwitter { - twitter?: ITwitter; -} - -function ConnectedTwitter({ twitter }: IConnectedTwitter) { - const { disconnectTwitter, getUserInfo } = useAppStore(); - - const handleDisconnect = async () => { - try { - await disconnectTwitter(); - - const userInfo = await getUserInfo(); - const { - twitterConnectedAt, - twitterId, - twitterProfileImageUrl, - twitterUsername, - } = userInfo; - - StorageService.updateLocalStorageWithObject('user', 'twitter', { - twitterConnectedAt, - twitterId, - twitterProfileImageUrl, - twitterUsername, - }); - - StorageService.removeLocalStorage('lastTwitterMetricsRefreshDate'); - } catch (error) { - console.error('Error handling disconnect:', error); - } - }; - - return ( -
- -
-
-

Twitter

- -
- -
-
- -
-

{twitter?.twitterUsername}

-

{`Connected ${moment( - twitter?.twitterConnectedAt - ).format('DD MMM yyyy')}`}

-
-
-
- Disconnect -
-
-
- ); -} - -export default ConnectedTwitter; diff --git a/src/components/pages/settings/DataAnalysis.tsx b/src/components/pages/settings/DataAnalysis.tsx deleted file mode 100644 index cd80b222..00000000 --- a/src/components/pages/settings/DataAnalysis.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import CustomModal from '../../global/CustomModal'; -import CustomButton from '../../global/CustomButton'; -import DatePeriodRange from '../../global/DatePeriodRange'; -import { BsClockHistory } from 'react-icons/bs'; -import useAppStore from '../../../store/useStore'; -import { FiInfo } from 'react-icons/fi'; -import { BiError } from 'react-icons/bi'; -import moment from 'moment'; -import { StorageService } from '../../../services/StorageService'; -import { IGuild, IUser } from '../../../utils/types'; - -export default function DataAnalysis() { - const [activePeriod, setActivePeriod] = useState(1); - const [guild, setGuild] = useState(); - const [analysisStateDate, setAnalysisStartDate] = useState(''); - const [open, setOpen] = useState(false); - const [datePeriod, setDatePeriod] = useState(''); - const [isDisabled, toggleDisabled] = useState(true); - const { guildInfo, updateAnalysisDatePeriod, getUserGuildInfo, guilds } = - useAppStore(); - - const handleActivePeriod = (dateRangeType: number | string) => { - let dateTime = ''; - switch (dateRangeType) { - case 1: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('35', 'days') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - case 2: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('3', 'months') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - case 3: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('6', 'months') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - case 4: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('1', 'year') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - default: - break; - } - setDatePeriod(dateTime); - toggleDisabled(false); - }; - - useEffect(() => { - const user = StorageService.readLocalStorage('user'); - if (user) { - setGuild(user.guild); - } - const start = moment(guildInfo.period, 'YYYY-MM-DD'); - const end = moment(); - - const datePeriod = Math.round(moment.duration(end.diff(start)).asMonths()); - - if (datePeriod <= 1) { - setActivePeriod(1); - } else if (datePeriod <= 3) { - setActivePeriod(2); - } else if (datePeriod > 3 && datePeriod <= 6) { - setActivePeriod(3); - } else { - setActivePeriod(4); - } - - setAnalysisStartDate(guildInfo.period); - }, [guildInfo]); - - const toggleModal = (e: boolean) => { - setOpen(e); - }; - - const submitNewDatePeriod = () => { - updateAnalysisDatePeriod(guild?.guildId, datePeriod).then((_res: any) => { - getUserGuildInfo(guild?.guildId); - setOpen(false); - }); - }; - - if (guilds.length === 0) { - return ( -
-

- It might take up to 6 hours to finish new data import. Once it is done - we will
send you a message on Discord. -

-

- - - There is no community connected at the moment. To be able to select - the date period, -
please connect your community - first. -
-

-
- - toggleModal(true)} - /> -
-
- ); - } - - return ( -
-

- It might take up to 6 hours to finish new data import. Once it is done - we will
send you a message on Discord. -

-

- - - Data analysis runs from:{' '} - {moment(analysisStateDate).format('DD MMMM yyyy')} - -

- -
- toggleModal(true)} - /> -
- -
- -

- We are changing date period for data analysis now -

-

- It might take up to 6 hours to finish new data import.{' '} -
Once it is done we will send you a - message on Discord. -

- -
-
-
- ); -} diff --git a/src/components/pages/settings/IntegrateDiscord.tsx b/src/components/pages/settings/IntegrateDiscord.tsx deleted file mode 100644 index 951025aa..00000000 --- a/src/components/pages/settings/IntegrateDiscord.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import useAppStore from '../../../store/useStore'; -import ConnectCommunities from './ConnectCommunities'; -import ConnectedCommunitiesList from './ConnectedCommunitiesList'; - -export default function IntegrateDiscord() { - const { guilds } = useAppStore(); - - return ( -
- - -
- ); -} From 95c80f917daeb5fb7488bf214ee341625cd68de6 Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 22 Dec 2023 18:05:40 +0300 Subject: [PATCH 05/30] remove settingSlice & authSlice --- src/store/slices/authSlice.ts | 73 --------------------- src/store/slices/settingSlice.ts | 108 ------------------------------- src/store/types/IAuth.ts | 34 ---------- src/store/types/ISetting.ts | 59 ----------------- src/store/useStore.ts | 4 -- 5 files changed, 278 deletions(-) delete mode 100644 src/store/slices/authSlice.ts delete mode 100644 src/store/slices/settingSlice.ts delete mode 100644 src/store/types/IAuth.ts delete mode 100644 src/store/types/ISetting.ts diff --git a/src/store/slices/authSlice.ts b/src/store/slices/authSlice.ts deleted file mode 100644 index 0e116464..00000000 --- a/src/store/slices/authSlice.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { StateCreator } from 'zustand'; -import IAuth, { IUser } from '../types/IAuth'; -import { conf } from '../../configs'; -import { axiosInstance } from '../../axiosInstance'; -import { StorageService } from '../../services/StorageService'; - -const BASE_URL = conf.API_BASE_URL; - -const createAuthSlice: StateCreator = (set, get) => ({ - isLoggedIn: false, - isLoading: false, - user: {}, - guildChannels: [], - - signUp: () => { - location.replace(`${BASE_URL}/auth/try-now`); - }, - - login: () => { - location.replace(`${BASE_URL}/auth/login`); - }, - - loginWithDiscord: (user: IUser) => - set(() => { - StorageService.writeLocalStorage('user', { - guild: { - guildId: user.guildId, - guildName: user.guildName, - }, - token: { - accessToken: user.accessToken, - accessExp: user.accessExp, - refreshToken: user.refreshToken, - refreshExp: user.refreshExp, - }, - }); - - return { user }; - }), - - fetchGuildChannels: async (guild_id: string) => { - try { - set(() => ({ isLoading: true })); - const { data } = await axiosInstance.get(`/guilds/${guild_id}/channels`); - set({ guildChannels: [...data], isLoading: false }); - } catch (error) { - set(() => ({ isLoading: false })); - } - }, - - updateGuildById: async (guildId, period, selectedChannels) => { - try { - set(() => ({ isLoading: true })); - await axiosInstance.patch(`/guilds/${guildId}`, { - period, - selectedChannels: selectedChannels, - }); - set({ isLoading: false }); - } catch (error) { - set(() => ({ isLoading: false })); - } - }, - - changeEmail: async (emailAddress: string) => { - try { - await axiosInstance.patch(`/users/@me`, { - email: emailAddress, - }); - } catch (error) {} - }, -}); - -export default createAuthSlice; diff --git a/src/store/slices/settingSlice.ts b/src/store/slices/settingSlice.ts deleted file mode 100644 index 175ede5d..00000000 --- a/src/store/slices/settingSlice.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { StateCreator } from 'zustand'; -import { axiosInstance } from '../../axiosInstance'; -import ISetting from '../types/ISetting'; -import { conf } from '../../configs'; - -const BASE_URL = conf.API_BASE_URL; - -const createSettingSlice: StateCreator = (set, get) => ({ - isLoading: false, - isRefetchLoading: false, - guildInfo: {}, - userInfo: {}, - guildInfoByDiscord: {}, - guilds: [], - guildChannels: [], - getUserGuildInfo: async (guildId: string) => { - try { - set(() => ({ isLoading: true })); - const { data } = await axiosInstance.get(`/guilds/${guildId}`); - - set({ guildInfo: data, isLoading: false }); - } catch (error) { - set(() => ({ guildInfo: {}, isLoading: false })); - } - }, - getUserInfo: async () => { - try { - const { data } = await axiosInstance.get('/users/@me'); - set({ userInfo: data }); - return data; - } catch (error) {} - }, - getGuildInfoByDiscord: async (guildId) => { - try { - set(() => ({ isLoading: true })); - const { data } = await axiosInstance.get(`/guilds/${guildId}`); - set({ guildInfoByDiscord: data, isLoading: false }); - } catch (error) { - set(() => ({ isLoading: false })); - } - }, - updateSelectedChannels: async (guildId, selectedChannels) => { - try { - set(() => ({ isLoading: true })); - await axiosInstance.patch(`/guilds/${guildId}`, { - selectedChannels: selectedChannels, - }); - set({ isLoading: false }); - } catch (error) { - set(() => ({ isLoading: false })); - } - }, - patchGuildById: async (guildId, period, selectedChannels) => { - try { - await axiosInstance.patch(`/guilds/${guildId}`, { - period, - selectedChannels: selectedChannels, - }); - } catch (error) {} - }, - updateAnalysisDatePeriod: async (guildId, period) => { - try { - set(() => ({ isLoading: true })); - await axiosInstance.patch(`/guilds/${guildId}`, { - period, - }); - set({ isLoading: false }); - } catch (error) { - set(() => ({ isLoading: false })); - } - }, - getGuilds: async () => { - try { - const { data } = await axiosInstance.get(`/guilds?isDisconnected=false`); - set({ - guilds: [...data.results], - }); - } catch (error) {} - }, - disconnecGuildById: async (guildId, disconnectType) => { - try { - set(() => ({ isLoading: true })); - await axiosInstance.post(`/guilds/${guildId}/disconnect`, { - disconnectType: disconnectType, - }); - set({ isLoading: false }); - } catch (error) { - set(() => ({ isLoading: false })); - } - }, - connectNewGuild: async () => { - try { - location.replace(`${BASE_URL}/guilds/connect`); - } catch (error) {} - }, - - refetchGuildChannels: async (guild_id: string) => { - try { - set(() => ({ isRefetchLoading: true })); - const { data } = await axiosInstance.get(`/guilds/${guild_id}/channels`); - set({ guildChannels: [...data], isRefetchLoading: false }); - } catch (error) { - set(() => ({ isRefetchLoading: false })); - } - }, -}); - -export default createSettingSlice; diff --git a/src/store/types/IAuth.ts b/src/store/types/IAuth.ts deleted file mode 100644 index ab03f71c..00000000 --- a/src/store/types/IAuth.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IChannelWithoutId, IGuildChannels } from '../../utils/types'; - -export type IUser = { - readonly accessToken: string; - readonly accessExp: string; - readonly guildId: string; - readonly guildName: string; - readonly refreshExp: string; - readonly refreshToken: string; -}; - -export type ISubChannels = { - readonly id: string; - readonly name: string; - readonly canReadMessageHistoryAndViewChannel: boolean; - readonly parent_id: string; -}; - -export default interface IAuth { - user: IUser | {}; - isLoading: boolean; - isLoggedIn: boolean; - guildChannels: IGuildChannels[]; - signUp: () => void; - login: () => void; - loginWithDiscord: (user: IUser) => void; - fetchGuildChannels: (guild_id: string) => void; - updateGuildById: ( - guildId: string, - period: string, - selectedChannels: IChannelWithoutId[] - ) => any; - changeEmail: (email: string) => any; -} diff --git a/src/store/types/ISetting.ts b/src/store/types/ISetting.ts deleted file mode 100644 index 5de57107..00000000 --- a/src/store/types/ISetting.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { IChannelWithoutId, IGuildChannels } from '../../utils/types'; - -export type IGuildInfo = { - id?: string; - guildId?: string; - ownerId?: string; - name?: boolean; - period?: string; - selectedChannels?: IChannelWithoutId[]; -}; - -export type DISCONNECT_TYPE = 'soft' | 'hard'; - -export interface IUserInfo { - discordId: string; - email: string; - verified: boolean; - avatar: string; - twitterConnectedAt: string; - twitterId: string; - twitterProfileImageUrl: string; - twitterUsername: string; - twitterIsInProgress: boolean; - id: string; -} - -export default interface IGuildList extends IGuildInfo { - isInProgress?: boolean; - isDisconnected?: boolean; - connectedAt?: string; -} -export default interface ISetting { - isLoading: boolean; - isRefetchLoading: boolean; - guildInfo?: IGuildInfo | {}; - userInfo: IUserInfo | {}; - guildInfoByDiscord: {}; - guilds: IGuildList[]; - guildChannels: IGuildChannels[]; - getUserGuildInfo: (guildId: string) => void; - getUserInfo: () => any; - getGuildInfoByDiscord: (guildId: string) => void; - updateSelectedChannels: ( - guildId: string, - selectedChannels: IChannelWithoutId[] - ) => void; - patchGuildById: ( - guildId: string, - period: string, - selectedChannels: IChannelWithoutId[] - ) => any; - updateAnalysisDatePeriod: (guildId: string, period: string) => void; - getGuilds: () => void; - disconnecGuildById: ( - guildId: string, - disconnectType: DISCONNECT_TYPE - ) => void; - refetchGuildChannels: (guild_id: string) => void; -} diff --git a/src/store/useStore.ts b/src/store/useStore.ts index 63526092..16a674c6 100644 --- a/src/store/useStore.ts +++ b/src/store/useStore.ts @@ -1,7 +1,5 @@ import { create } from 'zustand'; -import createAuthSlice from './slices/authSlice'; import createChartSlice from './slices/chartSlice'; -import createSettingSlice from './slices/settingSlice'; import createBreakdownsSlice from './slices/breakdownsSlice'; import createMemberInteractionSlice from './slices/memberInteractionSlice'; import communityHealthSlice from './slices/communityHealthSlice'; @@ -11,9 +9,7 @@ import platformSlice from './slices/platformSlice'; import userSlice from './slices/userSlice'; const useAppStore = create()((...a) => ({ - ...createAuthSlice(...a), ...createChartSlice(...a), - ...createSettingSlice(...a), ...createBreakdownsSlice(...a), ...createMemberInteractionSlice(...a), ...communityHealthSlice(...a), From 9a0efb7a8236160ec766f6e96fedcb55e68b87fd Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 22 Dec 2023 18:08:54 +0300 Subject: [PATCH 06/30] remove old channelList component --- src/components/pages/login/ChannelList.tsx | 91 ---------------------- 1 file changed, 91 deletions(-) delete mode 100644 src/components/pages/login/ChannelList.tsx diff --git a/src/components/pages/login/ChannelList.tsx b/src/components/pages/login/ChannelList.tsx deleted file mode 100644 index 91c38db6..00000000 --- a/src/components/pages/login/ChannelList.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { FormControlLabel, Checkbox } from '@mui/material'; -import { FiAlertTriangle } from 'react-icons/fi'; -import { ISubChannels } from '../../../utils/types'; - -type IChannelListProps = { - guild: any; - showFlag: boolean; - onChange: (channelId: string, subChannelId: string, status: boolean) => void; - handleCheckAll: (guild: any, status: boolean) => void; -}; - -export default function ChannelList({ - guild, - onChange, - handleCheckAll, - showFlag, -}: IChannelListProps) { - const subChannelsList = ( - <> -

Channels

- {guild.subChannels.map((channel: ISubChannels, index: any) => ( -
-
- - onChange( - guild.channelId, - channel.channelId, - e.target.checked - ) - } - /> - } - label={channel.name} - /> - {showFlag && !channel.canReadMessageHistoryAndViewChannel ? ( -
- - - {!channel.canReadMessageHistoryAndViewChannel - ? 'Bot needs access' - : ''} - -
- ) : ( - '' - )} -
-
- ))} - - ); - - return ( -
-

{guild.title}

-
- item)} - color="secondary" - onChange={(e) => handleCheckAll(guild, e.target.checked)} - /> - } - /> - {subChannelsList} -
-
- ); -} - -ChannelList.defaultProps = { - showFlag: false, -}; From 7b99adbae36a78849d7dd95a8af2bb7e9b707d0b Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 22 Dec 2023 18:18:07 +0300 Subject: [PATCH 07/30] remove useless global component --- src/components/global/Accardion.tsx | 66 ------------------- src/components/global/Card.tsx | 28 -------- src/components/global/CustomDatePicker.tsx | 77 ---------------------- src/components/global/CustomModal.tsx | 60 ----------------- src/components/global/DatePeriodRange.tsx | 63 ------------------ tsconfig.json | 2 +- 6 files changed, 1 insertion(+), 295 deletions(-) delete mode 100644 src/components/global/Accardion.tsx delete mode 100644 src/components/global/Card.tsx delete mode 100644 src/components/global/CustomDatePicker.tsx delete mode 100644 src/components/global/CustomModal.tsx delete mode 100644 src/components/global/DatePeriodRange.tsx diff --git a/src/components/global/Accardion.tsx b/src/components/global/Accardion.tsx deleted file mode 100644 index e4400f2a..00000000 --- a/src/components/global/Accardion.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { ReactElement } from 'react'; -import { Accordion, AccordionDetails, AccordionSummary } from '@mui/material'; -import { MdExpandMore } from 'react-icons/md'; - -type AcProps = { - readonly title?: string; - childs: AcChildProps[]; -}; - -type AcChildProps = { - title: string; - id: string; - icon?: ReactElement; - detailsComponent: ReactElement; -}; - -export default function Accardion({ title, childs }: AcProps) { - const [expanded, setExpanded] = React.useState(false); - - const handleChange = - (panel: string) => (_event: React.SyntheticEvent, isExpanded: boolean) => { - setExpanded(isExpanded ? panel : false); - }; - - return ( - <> -

{title}

- {childs.map((el) => ( - - - } - aria-controls={`${el.id}-content`} - id={el.id} - > -
-
- {el.icon} -
-

{el.title}

-
-
- - {el.detailsComponent} - -
- ))} - - ); -} diff --git a/src/components/global/Card.tsx b/src/components/global/Card.tsx deleted file mode 100644 index fc0ae5c2..00000000 --- a/src/components/global/Card.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import Image from "next/image"; - -type Props = { - className?: string; - title: string; - srcImage: string; - srcWidth: number; -}; - -export default function Card({ className, title, srcImage, srcWidth }: Props) { - return ( -
-

{title}

-
-
- Picture of the author -
-
-
- ); -} - -Card.defaultProps = { - title: "", - srcWidth: "400", -}; diff --git a/src/components/global/CustomDatePicker.tsx b/src/components/global/CustomDatePicker.tsx deleted file mode 100644 index 45ede02b..00000000 --- a/src/components/global/CustomDatePicker.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React, { FC, RefObject, useState } from 'react'; -import '@hassanmojab/react-modern-calendar-datepicker/lib/DatePicker.css'; -import DatePicker, { - DayRange, -} from '@hassanmojab/react-modern-calendar-datepicker'; -import { FiCalendar } from 'react-icons/fi'; -import clsx from 'clsx'; -import moment from 'moment'; - -interface IProps { - placeholder?: string; - className: string; - onClick: any; -} - -const CustomDatePicker: FC = ({ - placeholder, - className, - onClick, -}): JSX.Element => { - const [dayRange, setDayRange] = useState({ - from: null, - to: null, - }); - - const renderCustomInput = ({ - ref, - }: { - ref: RefObject | any; - }) => ( -
- - -
- ); - - return ( - setDayRange(date)} - renderInput={renderCustomInput} - colorPrimary="#35B9B7" // added this - colorPrimaryLight="#D0FBF8" // and this - calendarPopperPosition="bottom" - /> - ); -}; - -CustomDatePicker.defaultProps = { - placeholder: 'Specific date', -}; - -export default CustomDatePicker; diff --git a/src/components/global/CustomModal.tsx b/src/components/global/CustomModal.tsx deleted file mode 100644 index 3209fa9a..00000000 --- a/src/components/global/CustomModal.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Dialog, DialogTitle, DialogContent } from '@mui/material'; -import { IoClose } from 'react-icons/io5'; - -type IModalProps = { - isOpen: boolean; - toggleModal: (arg0: boolean) => void; - children: any; - hasClose: boolean; -}; -export default function ConfirmModal({ - isOpen, - toggleModal, - children, - hasClose, - ...props -}: IModalProps) { - const handleClose = () => { - toggleModal(false); - }; - return ( - <> - - {hasClose ? ( - - - - ) : ( - '' - )} - {children} - - - ); -} diff --git a/src/components/global/DatePeriodRange.tsx b/src/components/global/DatePeriodRange.tsx deleted file mode 100644 index 826d0b95..00000000 --- a/src/components/global/DatePeriodRange.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import clsx from 'clsx'; -import React, { useState } from 'react'; -import CustomDatePicker from './CustomDatePicker'; - -type dateItems = { - title: string; - icon?: JSX.Element; - value: any; -}; - -const datePeriod: dateItems[] = [ - { - title: 'Last 35 days', - value: 1, - }, - { - title: '3M', - value: 2, - }, - { - title: '6M', - value: 3, - }, - { - title: '1Y', - value: 4, - }, -]; - -type datePeriodRangeProps = { - activePeriod: string | number; - onChangeActivePeriod: (e: number) => void; -}; - -export default function DatePeriodRange({ - activePeriod, - onChangeActivePeriod, -}: datePeriodRangeProps) { - return ( -
-
    - {datePeriod.length > 0 - ? datePeriod.map((el) => ( -
  • onChangeActivePeriod(el.value)} - > - {el.icon ? el.icon : ''} -
    {el.title}
    -
  • - )) - : ''} -
-
- ); -} diff --git a/tsconfig.json b/tsconfig.json index b194c6be..e313683d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,5 +22,5 @@ "jest.config.js", "jest.setup.js" ], - "exclude": ["node_modules", "./src/components/global/CustomDatePicker.tsx"] + "exclude": ["node_modules"] } From 92d4f96d62b0aab9a7410a703ba2b814b90f8255 Mon Sep 17 00:00:00 2001 From: zuies Date: Mon, 25 Dec 2023 13:10:50 +0300 Subject: [PATCH 08/30] create announcements initial route --- src/pages/announcement/index.tsx | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/pages/announcement/index.tsx diff --git a/src/pages/announcement/index.tsx b/src/pages/announcement/index.tsx new file mode 100644 index 00000000..a78b8b3a --- /dev/null +++ b/src/pages/announcement/index.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +function index() { + return
index
; +} + +export default index; From fc6fb47edf12529e98c14fd90a5ca256dd16a85b Mon Sep 17 00:00:00 2001 From: zuies Date: Tue, 26 Dec 2023 16:11:37 +0300 Subject: [PATCH 09/30] create shared tableContainer component --- src/components/layouts/Sidebar.tsx | 10 +++++ .../TcTableContainer/TcTableBody.spec.tsx | 30 +++++++++++++ .../shared/TcTableContainer/TcTableBody.tsx | 41 ++++++++++++++++++ .../TcTableContainer/TcTableCell.spec.tsx | 10 +++++ .../shared/TcTableContainer/TcTableCell.tsx | 25 +++++++++++ .../TcTableContainer.spec.tsx | 31 +++++++++++++ .../TcTableContainer/TcTableContainer.tsx | 40 +++++++++++++++++ .../TcTableContainer/TcTableHead.spec.tsx | 17 ++++++++ .../shared/TcTableContainer/TcTableHead.tsx | 23 ++++++++++ .../TcTableContainer/TcTableRow.spec.tsx | 14 ++++++ .../shared/TcTableContainer/TcTableRow.tsx | 35 +++++++++++++++ .../shared/TcTableContainer/index.ts | 3 ++ src/pages/announcement/index.tsx | 7 --- src/pages/announcements/index.tsx | 43 +++++++++++++++++++ src/styles/globals.css | 3 ++ 15 files changed, 325 insertions(+), 7 deletions(-) create mode 100644 src/components/shared/TcTableContainer/TcTableBody.spec.tsx create mode 100644 src/components/shared/TcTableContainer/TcTableBody.tsx create mode 100644 src/components/shared/TcTableContainer/TcTableCell.spec.tsx create mode 100644 src/components/shared/TcTableContainer/TcTableCell.tsx create mode 100644 src/components/shared/TcTableContainer/TcTableContainer.spec.tsx create mode 100644 src/components/shared/TcTableContainer/TcTableContainer.tsx create mode 100644 src/components/shared/TcTableContainer/TcTableHead.spec.tsx create mode 100644 src/components/shared/TcTableContainer/TcTableHead.tsx create mode 100644 src/components/shared/TcTableContainer/TcTableRow.spec.tsx create mode 100644 src/components/shared/TcTableContainer/TcTableRow.tsx create mode 100644 src/components/shared/TcTableContainer/index.ts delete mode 100644 src/pages/announcement/index.tsx create mode 100644 src/pages/announcements/index.tsx diff --git a/src/components/layouts/Sidebar.tsx b/src/components/layouts/Sidebar.tsx index 3fb55c95..8d65e1fc 100644 --- a/src/components/layouts/Sidebar.tsx +++ b/src/components/layouts/Sidebar.tsx @@ -11,6 +11,7 @@ import { conf } from '../../configs/index'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faUserGroup, faHeartPulse } from '@fortawesome/free-solid-svg-icons'; +import { MdOutlineAnnouncement } from 'react-icons/md'; import { useRouter } from 'next/router'; import Link from 'next/link'; @@ -60,6 +61,15 @@ const Sidebar = () => { /> ), }, + { + name: 'Announcements', + path: '/announcements', + icon: ( + + ), + }, { name: 'Community Settings', path: '/community-settings', diff --git a/src/components/shared/TcTableContainer/TcTableBody.spec.tsx b/src/components/shared/TcTableContainer/TcTableBody.spec.tsx new file mode 100644 index 00000000..2e8d1bb4 --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableBody.spec.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcTableBody from './TcTableBody'; + +describe('TcTableBody', () => { + const mockRowItems = [ + { Name: 'Alice', Age: 28, Location: 'New York' }, + { Name: 'Bob', Age: 34, Location: 'San Francisco' }, + ]; + + it('renders correctly with rowItems', () => { + render( + + +
+ ); + const tableRows = screen.getAllByRole('row'); + expect(tableRows.length).toBe(mockRowItems.length); + }); + + it('applies alternate background color for rows', () => { + render( + + +
+ ); + const firstRow = screen.getAllByRole('row')[0]; + expect(firstRow).toHaveClass('bg-gray-100'); + }); +}); diff --git a/src/components/shared/TcTableContainer/TcTableBody.tsx b/src/components/shared/TcTableContainer/TcTableBody.tsx new file mode 100644 index 00000000..67b08449 --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableBody.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { TableBody, TableBodyProps } from '@mui/material'; +import TcTableRow from './TcTableRow'; + +interface ITcTableBodyProps extends TableBodyProps { + rowItems: { [key: string]: any }[]; +} + +/** + * TcTableBody Component + * + * Renders a Material-UI TableBody with custom row items. + * Each row is rendered using the TcTableRow component. + * + * Props: + * - rowItems: Array of objects, each representing data for a single row. + * + * @param {ITcTableBodyProps} props - Props including rowItems and other TableBodyProps + */ + +function TcTableBody({ rowItems, ...props }: ITcTableBodyProps) { + return ( + + {rowItems.map((row, index) => ( + + ))} + + ); +} + +TcTableBody.defaultProps = { + rowItems: [], +}; + +export default TcTableBody; diff --git a/src/components/shared/TcTableContainer/TcTableCell.spec.tsx b/src/components/shared/TcTableContainer/TcTableCell.spec.tsx new file mode 100644 index 00000000..a605d4a4 --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableCell.spec.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcTableCell from './TcTableCell'; + +describe('TcTableCell', () => { + it('renders the children content', () => { + render(Test Content); + expect(screen.getByText('Test Content')).toBeInTheDocument(); + }); +}); diff --git a/src/components/shared/TcTableContainer/TcTableCell.tsx b/src/components/shared/TcTableContainer/TcTableCell.tsx new file mode 100644 index 00000000..0717b3d2 --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableCell.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { TableCell, TableCellProps } from '@mui/material'; + +interface ITcTableCellProps extends TableCellProps { + children: React.ReactNode; +} + +/** + * TcTableCell Component + * + * Custom TableCell component that extends Material-UI's TableCell. + * It can be used within Material-UI's Table components to display cell data. + * + * Props: + * - children: ReactNode - The content of the cell. + * - Other props inherited from Material-UI TableCellProps. + * + * @param {ITcTableCellProps} props - Props including children and TableCellProps + */ + +function TcTableCell({ children, ...props }: ITcTableCellProps) { + return {children}; +} + +export default TcTableCell; diff --git a/src/components/shared/TcTableContainer/TcTableContainer.spec.tsx b/src/components/shared/TcTableContainer/TcTableContainer.spec.tsx new file mode 100644 index 00000000..bca231fe --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableContainer.spec.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcTableContainer from './TcTableContainer'; + +describe('TcTableContainer', () => { + const mockHeaders = ['Header 1', 'Header 2']; + const mockBodyRowItems = [ + { Column1: 'Row 1, Column 1', Column2: 'Row 1, Column 2' }, + { Column1: 'Row 2, Column 1', Column2: 'Row 2, Column 2' }, + ]; + + it('renders the table with body row items', () => { + render(); + + // Check if each row text content is present in the document + mockBodyRowItems.forEach((rowData) => { + Object.values(rowData).forEach((cellText) => { + const cell = screen.getByText(cellText); + expect(cell).toBeInTheDocument(); + }); + }); + }); + + it('applies custom classes for border separation and spacing', () => { + render( + + ); + const table = screen.getByRole('table'); + expect(table).toHaveClass('border-separate border-spacing-y-2'); + }); +}); diff --git a/src/components/shared/TcTableContainer/TcTableContainer.tsx b/src/components/shared/TcTableContainer/TcTableContainer.tsx new file mode 100644 index 00000000..9e580c73 --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableContainer.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Table, TableProps } from '@mui/material'; +import TcTableHead from './TcTableHead'; +import TcTableBody from './TcTableBody'; + +interface ITcTableContainerProps extends TableProps { + headers?: string[]; + bodyRowItems?: any[]; +} + +/** + * TcTableContainer Component + * + * Custom Table component that extends Material-UI's Table. + * It can be used to display tabular data with optional custom border separation and spacing. + * + * Props: + * - headers: Array of strings - The table column headers. + * - bodyRowItems: Array of objects - The data for table rows. + * - Other props inherited from Material-UI TableProps. + * + * @param {ITcTableContainerProps} props - Props including headers, bodyRowItems, and TableProps + */ + +function TcTableContainer({ + headers, + bodyRowItems, + ...props +}: ITcTableContainerProps) { + return ( + + {headers && headers.length > 0 && } + {bodyRowItems && bodyRowItems.length > 0 && ( + + )} +
+ ); +} + +export default TcTableContainer; diff --git a/src/components/shared/TcTableContainer/TcTableHead.spec.tsx b/src/components/shared/TcTableContainer/TcTableHead.spec.tsx new file mode 100644 index 00000000..60d30645 --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableHead.spec.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcTableHead from './TcTableHead'; + +describe('TcTableHead', () => { + const mockHeaders = ['Header 1', 'Header 2']; + + it('renders the table head with headers', () => { + render(); + + // Check if each header text content is present in the document + mockHeaders.forEach((headerText) => { + const header = screen.getByText(headerText); + expect(header).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/shared/TcTableContainer/TcTableHead.tsx b/src/components/shared/TcTableContainer/TcTableHead.tsx new file mode 100644 index 00000000..ef6cc01b --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableHead.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { TableHead, TableHeadProps, TableRow, TableCell } from '@mui/material'; +import TcTableRow from './TcTableRow'; + +interface ITcTableHeadProps extends TableHeadProps { + headers: string[]; +} + +/** + * Component to render the table head with headers. + * + * @param {ITcTableHeadProps} props - The component props. + */ + +function TcTableHead({ headers, ...props }: ITcTableHeadProps) { + return ( + + + + ); +} + +export default TcTableHead; diff --git a/src/components/shared/TcTableContainer/TcTableRow.spec.tsx b/src/components/shared/TcTableContainer/TcTableRow.spec.tsx new file mode 100644 index 00000000..f9eaa7cb --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableRow.spec.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcTableCell from './TcTableCell'; + +describe('TcTableCell', () => { + it('renders correctly with children', () => { + // Render the TcTableCell component with some children + render(Sample Content); + + // Check if the rendered content is present + const cellContent = screen.getByText('Sample Content'); + expect(cellContent).toBeInTheDocument(); + }); +}); diff --git a/src/components/shared/TcTableContainer/TcTableRow.tsx b/src/components/shared/TcTableContainer/TcTableRow.tsx new file mode 100644 index 00000000..ebe64525 --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableRow.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { TableRow, TableRowProps } from '@mui/material'; +import TcTableCell from './TcTableCell'; + +interface ITcTableRowProps extends TableRowProps { + rowItem: { [key: string]: any }; + customRenderers?: { [key: string]: (value: any) => React.ReactNode }; +} + +/** + * Component to render a table row with custom rendering options. + * + * @param {ITcTableRowProps} props - The component props. + */ + +function TcTableRow({ rowItem, customRenderers, ...props }: ITcTableRowProps) { + return ( + + {rowItem && + Object.entries(rowItem).map(([key, value], index) => { + const CustomRenderer = customRenderers?.[key]; + return ( + + {CustomRenderer ? CustomRenderer(value) : value} + + ); + })} + + ); +} + +export default TcTableRow; diff --git a/src/components/shared/TcTableContainer/index.ts b/src/components/shared/TcTableContainer/index.ts new file mode 100644 index 00000000..e1a070a6 --- /dev/null +++ b/src/components/shared/TcTableContainer/index.ts @@ -0,0 +1,3 @@ +import { default as TcTableContainer } from './TcTableContainer'; + +export default TcTableContainer; diff --git a/src/pages/announcement/index.tsx b/src/pages/announcement/index.tsx deleted file mode 100644 index a78b8b3a..00000000 --- a/src/pages/announcement/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -function index() { - return
index
; -} - -export default index; diff --git a/src/pages/announcements/index.tsx b/src/pages/announcements/index.tsx new file mode 100644 index 00000000..71dd0fb3 --- /dev/null +++ b/src/pages/announcements/index.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { defaultLayout } from '../../layouts/defaultLayout'; +import TcBoxContainer from '../../components/shared/TcBox/TcBoxContainer'; +import SEO from '../../components/global/SEO'; +import TcText from '../../components/shared/TcText'; +import TcButton from '../../components/shared/TcButton'; +import { BsPlus } from 'react-icons/bs'; +import TcTableContainer from '../../components/shared/TcTableContainer'; + +const bodyRowItems = [ + { Name: 'Alice', Age: 28, Location: 'New York' }, + { Name: 'Bob', Age: 34, Location: 'San Francisco' }, + { Name: 'Carol', Age: 23, Location: 'Miami' }, +]; + +function Index() { + return ( + <> + +
+ +
+ + } + variant="outlined" + /> +
+ +
+ } + /> +
+ + ); +} + +Index.pageLayout = defaultLayout; + +export default Index; diff --git a/src/styles/globals.css b/src/styles/globals.css index a09541cc..e677196b 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -78,3 +78,6 @@ body { .highcharts-credits { pointer-events: none !important; } +.no-border td { + border: 0; +} From 138f9f569d72865ff68e08ebcb0e1268ae22815c Mon Sep 17 00:00:00 2001 From: zuies Date: Wed, 27 Dec 2023 13:25:13 +0300 Subject: [PATCH 10/30] public message container done --- .../create/TcIconContainer.spec.tsx | 14 +++ .../announcements/create/TcIconContainer.tsx | 31 +++++++ .../TcPrivateMessaageContainer.tsx | 86 +++++++++++++++++++ .../TcPrivateMessaageContainer/index.ts | 3 + .../TcPublicMessaageContainer.tsx | 86 +++++++++++++++++++ .../create/publicMessageContainer/index.ts | 3 + src/components/shared/TcSelect/TcSelect.tsx | 73 ++++++++-------- .../create-new-announcements.tsx | 73 ++++++++++++++++ src/pages/announcements/index.tsx | 4 + 9 files changed, 338 insertions(+), 35 deletions(-) create mode 100644 src/components/announcements/create/TcIconContainer.spec.tsx create mode 100644 src/components/announcements/create/TcIconContainer.tsx create mode 100644 src/components/announcements/create/TcPrivateMessaageContainer/TcPrivateMessaageContainer.tsx create mode 100644 src/components/announcements/create/TcPrivateMessaageContainer/index.ts create mode 100644 src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx create mode 100644 src/components/announcements/create/publicMessageContainer/index.ts create mode 100644 src/pages/announcements/create-new-announcements.tsx diff --git a/src/components/announcements/create/TcIconContainer.spec.tsx b/src/components/announcements/create/TcIconContainer.spec.tsx new file mode 100644 index 00000000..4c3f102b --- /dev/null +++ b/src/components/announcements/create/TcIconContainer.spec.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcIconContainer from './TcIconContainer'; + +describe('TcIconContainer', () => { + it('renders its children', () => { + render( + +
Test Child
+
+ ); + expect(screen.getByText('Test Child')).toBeInTheDocument(); + }); +}); diff --git a/src/components/announcements/create/TcIconContainer.tsx b/src/components/announcements/create/TcIconContainer.tsx new file mode 100644 index 00000000..4b5c68a0 --- /dev/null +++ b/src/components/announcements/create/TcIconContainer.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +/** + * Interface defining the properties for TcIconContainer. + * @interface ITcIconContainerProps + */ +interface ITcIconContainerProps { + /** + * Children elements to be rendered inside the container. + * This should be a single React element, typically an icon or a small component. + * @type {React.ReactElement} + */ + children: React.ReactElement; +} + +/** + * A container component designed to display its children in a circular, centered fashion. + * Ideal for icons or small elements. + * + * @param {ITcIconContainerProps} props - The properties passed to the component. + * @returns {JSX.Element} A div element with applied styling and containing the children. + */ +function TcIconContainer({ children }: ITcIconContainerProps): JSX.Element { + return ( +
+ {children} +
+ ); +} + +export default TcIconContainer; diff --git a/src/components/announcements/create/TcPrivateMessaageContainer/TcPrivateMessaageContainer.tsx b/src/components/announcements/create/TcPrivateMessaageContainer/TcPrivateMessaageContainer.tsx new file mode 100644 index 00000000..516a7eaa --- /dev/null +++ b/src/components/announcements/create/TcPrivateMessaageContainer/TcPrivateMessaageContainer.tsx @@ -0,0 +1,86 @@ +import React, { useState } from 'react'; +import TcText from '../../../shared/TcText'; +import { MdOutlineAnnouncement } from 'react-icons/md'; +import TcIconContainer from '../TcIconContainer'; +import TcButton from '../../../shared/TcButton'; +import TcSelect from '../../../shared/TcSelect'; +import { FormControl, InputLabel, SelectChangeEvent } from '@mui/material'; +import TcInput from '../../../shared/TcInput'; + +const mockPublicChannels = [ + { + label: 'test', + value: 1, + }, + { + label: 'test2', + value: 2, + }, +]; + +function TcPrivateMessaageContainer() { + const [selectedChannels, setSelectedChannels] = useState([]); + const [message, setMessage] = useState(''); + + const handleSelectChange = (event: SelectChangeEvent) => { + setSelectedChannels(event.target.value as number[]); + }; + + const handleChange = (event: React.ChangeEvent) => { + setMessage(event.target.value); + }; + + return ( +
+
+
+ + + + +
+ +
+
+ + Select Channels + + (selected as number[]) + .map( + (value) => + mockPublicChannels.find( + (channel) => channel.value === value + )?.label + ) + .join(', ') + } + onChange={(event) => handleSelectChange(event)} + /> + + + + +
+
+ ); +} + +export default TcPrivateMessaageContainer; diff --git a/src/components/announcements/create/TcPrivateMessaageContainer/index.ts b/src/components/announcements/create/TcPrivateMessaageContainer/index.ts new file mode 100644 index 00000000..800fd209 --- /dev/null +++ b/src/components/announcements/create/TcPrivateMessaageContainer/index.ts @@ -0,0 +1,3 @@ +import { default as TcPrivateMessaageContainer } from './TcPrivateMessaageContainer'; + +export default TcPrivateMessaageContainer; diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx new file mode 100644 index 00000000..013ef562 --- /dev/null +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx @@ -0,0 +1,86 @@ +import React, { useState } from 'react'; +import TcText from '../../../shared/TcText'; +import { MdAnnouncement } from 'react-icons/md'; +import TcIconContainer from '../TcIconContainer'; +import TcButton from '../../../shared/TcButton'; +import TcSelect from '../../../shared/TcSelect'; +import { FormControl, InputLabel, SelectChangeEvent } from '@mui/material'; +import TcInput from '../../../shared/TcInput'; + +const mockPublicChannels = [ + { + label: 'test', + value: 1, + }, + { + label: 'test2', + value: 2, + }, +]; + +function TcPublicMessaageContainer() { + const [selectedChannels, setSelectedChannels] = useState([]); + const [message, setMessage] = useState(''); + + const handleSelectChange = (event: SelectChangeEvent) => { + setSelectedChannels(event.target.value as number[]); + }; + + const handleChange = (event: React.ChangeEvent) => { + setMessage(event.target.value); + }; + + return ( +
+
+
+ + + + +
+ +
+
+ + Select Channels + + (selected as number[]) + .map( + (value) => + mockPublicChannels.find( + (channel) => channel.value === value + )?.label + ) + .join(', ') + } + onChange={(event) => handleSelectChange(event)} + /> + + + + +
+
+ ); +} + +export default TcPublicMessaageContainer; diff --git a/src/components/announcements/create/publicMessageContainer/index.ts b/src/components/announcements/create/publicMessageContainer/index.ts new file mode 100644 index 00000000..55c8ddb5 --- /dev/null +++ b/src/components/announcements/create/publicMessageContainer/index.ts @@ -0,0 +1,3 @@ +import { default as TcPublicMessaageContainer } from './TcPublicMessaageContainer'; + +export default TcPublicMessaageContainer; diff --git a/src/components/shared/TcSelect/TcSelect.tsx b/src/components/shared/TcSelect/TcSelect.tsx index d911cd2d..abfa847f 100644 --- a/src/components/shared/TcSelect/TcSelect.tsx +++ b/src/components/shared/TcSelect/TcSelect.tsx @@ -1,45 +1,48 @@ +import { MenuItem, Select, SelectProps } from '@mui/material'; +import React, { ReactElement } from 'react'; +import { IconType } from 'react-icons'; +import TcText from '../TcText'; + /** - * TcSelect Component - * - * This component is a wrapper around Material-UI's Select component. - * It provides a dropdown select box functionality. - * - * Props: - * - options: Array of objects with 'value' and 'label' keys. These are used to populate the dropdown menu. - * - * Example Usage: - * + * Interface for TcSelect props */ - -import React, { useState } from 'react'; -import Select, { SelectChangeEvent } from '@mui/material/Select'; -import MenuItem from '@mui/material/MenuItem'; - -interface TcSelectProps { - options: { - value: string; +interface ITcSelectProps extends SelectProps { + /** + * options - Array of option objects for the select dropdown + * Each object can have: + * - value (string | number): The value of the option + * - label (string): The display label for the option + * - icon (ReactElement): Optional icon to display alongside the label + */ + options: Array<{ + value: string | number; label: string; - }[]; + icon?: ReactElement; + disabled?: boolean; + }>; } -function TcSelect({ options, ...props }: TcSelectProps) { - const [value, setValue] = useState(''); - - const handleChange = (event: SelectChangeEvent) => { - const newValue = event.target.value as string; - setValue(newValue); - }; +/** + * TcSelect is a custom select component built on Material-UI's Select component. + * It allows displaying a list of options with optional icons. + * + * @param {ITcSelectProps} props - The props for the component + * @returns {ReactElement} The TcSelect component + */ +function TcSelect({ options, ...props }: ITcSelectProps): ReactElement { return ( - + {options.map((option, index) => ( + +
+ {option.icon} + +
))} diff --git a/src/pages/announcements/create-new-announcements.tsx b/src/pages/announcements/create-new-announcements.tsx new file mode 100644 index 00000000..f42b9be9 --- /dev/null +++ b/src/pages/announcements/create-new-announcements.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { defaultLayout } from '../../layouts/defaultLayout'; +import SEO from '../../components/global/SEO'; +import TcBoxContainer from '../../components/shared/TcBox/TcBoxContainer'; +import TcText from '../../components/shared/TcText'; +import TcSelect from '../../components/shared/TcSelect'; +import { FormControl, InputLabel } from '@mui/material'; +import { BsDiscord, BsTelegram } from 'react-icons/bs'; +import TcPublicMessaageContainer from '../../components/announcements/create/publicMessageContainer'; +import TcPrivateMessaageContainer from '../../components/announcements/create/TcPrivateMessaageContainer'; + +const announcementsPlatforms = [ + { + label: 'Discord', + value: '1', + icon: , + }, + { + label: 'Telegram(TBA)', + value: '2', + disabled: true, + icon: , + }, +]; + +function CreateNewAnnouncements() { + return ( + <> + +
+ +
+
+ + +
+ + + Select Platform + + + +
+ + +
+ } + /> +
+ + ); +} + +CreateNewAnnouncements.pageLayout = defaultLayout; + +export default CreateNewAnnouncements; diff --git a/src/pages/announcements/index.tsx b/src/pages/announcements/index.tsx index 71dd0fb3..cb74d1ea 100644 --- a/src/pages/announcements/index.tsx +++ b/src/pages/announcements/index.tsx @@ -6,6 +6,7 @@ import TcText from '../../components/shared/TcText'; import TcButton from '../../components/shared/TcButton'; import { BsPlus } from 'react-icons/bs'; import TcTableContainer from '../../components/shared/TcTableContainer'; +import router from 'next/router'; const bodyRowItems = [ { Name: 'Alice', Age: 28, Location: 'New York' }, @@ -27,6 +28,9 @@ function Index() { text="Create Announcement" startIcon={} variant="outlined" + onClick={() => + router.push('/announcements/create-new-announcements') + } /> From 2c21184657db225098b98ed73b28bb291b7f906e Mon Sep 17 00:00:00 2001 From: zuies Date: Wed, 27 Dec 2023 13:30:45 +0300 Subject: [PATCH 11/30] add TcSwitch component --- src/components/shared/TcSwitch.spec.tsx | 16 +++++++++++ src/components/shared/TcSwitch.tsx | 37 +++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/components/shared/TcSwitch.spec.tsx create mode 100644 src/components/shared/TcSwitch.tsx diff --git a/src/components/shared/TcSwitch.spec.tsx b/src/components/shared/TcSwitch.spec.tsx new file mode 100644 index 00000000..3988c15c --- /dev/null +++ b/src/components/shared/TcSwitch.spec.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import TcSwitch from './TcSwitch'; + +describe('TcSwitch', () => { + test('it should toggle switch', () => { + const handleChange = jest.fn(); + const { getByRole } = render(); + + const switchControl = getByRole('checkbox'); + expect(switchControl).not.toBeChecked(); + + fireEvent.click(switchControl); + expect(handleChange).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/shared/TcSwitch.tsx b/src/components/shared/TcSwitch.tsx new file mode 100644 index 00000000..6d980173 --- /dev/null +++ b/src/components/shared/TcSwitch.tsx @@ -0,0 +1,37 @@ +import { Switch, SwitchProps } from '@mui/material'; +import React from 'react'; + +interface ITcSwitchProps extends SwitchProps {} + +/** + * `TcSwitch` Component + * + * This component is a wrapper around Material-UI's `Switch` component. + * It can be used anywhere a Material-UI Switch would be used. It accepts all props + * that a standard Material-UI Switch accepts. + * + * Usage: + * + * + * Props: + * - All props available to Material-UI's `Switch` component. + * - `checked`: Boolean indicating whether the switch is on or off. + * - `onChange`: Function to handle the change event when the switch is toggled. + * + * Example: + * ``` + * { this.setState({ isChecked: e.target.checked }) }} + * /> + * ``` + * + * For more details on Material-UI's `Switch` props, + * see: https://mui.com/api/switch/ + */ + +function TcSwitch({ ...props }: ITcSwitchProps) { + return ; +} + +export default TcSwitch; From f6a7cb31263ae802392fe1e1c0b2e95c8ba4104b Mon Sep 17 00:00:00 2001 From: zuies Date: Wed, 3 Jan 2024 15:09:54 +0300 Subject: [PATCH 12/30] complete create-announcements --- package-lock.json | 389 ++++++++++++++---- package.json | 3 + .../TcPrivateMessaageContainer.tsx | 86 ---- .../TcPrivateMessaageContainer.spec.tsx | 43 ++ .../TcPrivateMessaageContainer.tsx | 195 +++++++++ .../index.ts | 0 .../TcPublicMessaageContainer.spec.tsx | 32 ++ .../TcPublicMessaageContainer.tsx | 17 +- .../TcDateTimePopover.tsx | 97 +++++ .../TcScheduleAnnouncement.spec.tsx | 11 + .../TcScheduleAnnouncement.tsx | 90 ++++ .../create/scheduleAnnouncement/index.ts | 3 + .../selectPlatform/TcSelectPlatform.tsx | 47 +++ .../create/selectPlatform/index.ts | 3 + src/components/shared/TcBreadcrumbs.tsx | 63 +-- src/components/shared/TcLink.tsx | 2 +- src/components/shared/TcTabs/TcTab/TcTab.tsx | 10 + src/components/shared/TcTabs/TcTab/index.ts | 3 + src/components/shared/TcTabs/TcTabs.tsx | 26 ++ src/components/shared/TcTabs/index.ts | 3 + .../create-new-announcements.tsx | 78 ++-- .../community-settings/platform/index.tsx | 1 + src/utils/theme.ts | 46 +-- 23 files changed, 968 insertions(+), 280 deletions(-) delete mode 100644 src/components/announcements/create/TcPrivateMessaageContainer/TcPrivateMessaageContainer.tsx create mode 100644 src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.spec.tsx create mode 100644 src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx rename src/components/announcements/create/{TcPrivateMessaageContainer => privateMessaageContainer}/index.ts (100%) create mode 100644 src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.spec.tsx create mode 100644 src/components/announcements/create/scheduleAnnouncement/TcDateTimePopover.tsx create mode 100644 src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.spec.tsx create mode 100644 src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx create mode 100644 src/components/announcements/create/scheduleAnnouncement/index.ts create mode 100644 src/components/announcements/create/selectPlatform/TcSelectPlatform.tsx create mode 100644 src/components/announcements/create/selectPlatform/index.ts create mode 100644 src/components/shared/TcTabs/TcTab/TcTab.tsx create mode 100644 src/components/shared/TcTabs/TcTab/index.ts create mode 100644 src/components/shared/TcTabs/TcTabs.tsx create mode 100644 src/components/shared/TcTabs/index.ts diff --git a/package-lock.json b/package-lock.json index 18367f7c..9ea7cd8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@amplitude/analytics-browser": "^1.9.4", + "@date-io/date-fns": "^2.17.0", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@fortawesome/fontawesome-svg-core": "^6.2.0", @@ -19,6 +20,7 @@ "@hassanmojab/react-modern-calendar-datepicker": "^3.1.7", "@mui/lab": "^5.0.0-alpha.121", "@mui/material": "^5.10.13", + "@mui/x-date-pickers": "^6.18.6", "@sentry/nextjs": "^7.50.0", "@types/node": "18.11.9", "@types/react": "18.0.25", @@ -30,6 +32,7 @@ "axios": "^1.2.2", "clsx": "^1.2.1", "d3-force": "^3.0.0", + "date-fns": "^2.30.0", "eslint": "8.27.0", "eslint-config-next": "13.0.2", "eslint-config-prettier": "^8.6.0", @@ -600,11 +603,11 @@ } }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz", + "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -662,6 +665,27 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@date-io/core": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.17.0.tgz", + "integrity": "sha512-+EQE8xZhRM/hsY0CDTVyayMDDY5ihc4MqXCrPxooKw19yAzUIC6uUqsZeaOFNL9YKTNxYKrJP5DFgE8o5xRCOw==" + }, + "node_modules/@date-io/date-fns": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.17.0.tgz", + "integrity": "sha512-L0hWZ/mTpy3Gx/xXJ5tq5CzHo0L7ry6KEO9/w/JWiFWFLZgiNVo3ex92gOl3zmzjHqY/3Ev+5sehAr8UnGLEng==", + "dependencies": { + "@date-io/core": "^2.17.0" + }, + "peerDependencies": { + "date-fns": "^2.0.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + } + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.10.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz", @@ -844,18 +868,39 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.1.tgz", - "integrity": "sha512-LSqwPZkK3rYfD7GKoIeExXOyYx6Q1O4iqZWwIehDNuv3Dv425FIAE8PRwtAx1imEolFTHgBEcoFHm9MDnYgPCg==" + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.2.tgz", + "integrity": "sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==", + "dependencies": { + "@floating-ui/utils": "^0.1.3" + } }, "node_modules/@floating-ui/dom": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.1.tgz", - "integrity": "sha512-Rt45SmRiV8eU+xXSB9t0uMYiQ/ZWGE/jumse2o3i5RGlyvcbqOF4q+1qBnzLE2kZ5JGhq0iMkcGXUKbFe7MpTA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", + "dependencies": { + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz", + "integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==", "dependencies": { - "@floating-ui/core": "^1.2.1" + "@floating-ui/dom": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, + "node_modules/@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz", @@ -2077,11 +2122,11 @@ } }, "node_modules/@mui/types": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.3.tgz", - "integrity": "sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==", + "version": "7.2.11", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.11.tgz", + "integrity": "sha512-KWe/QTEsFFlFSH+qRYf3zoFEj3z67s+qAuSnMMg+gFwbxG7P96Hm6g300inQL1Wy///gSRb8juX7Wafvp93m3w==", "peerDependencies": { - "@types/react": "*" + "@types/react": "^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -2090,13 +2135,12 @@ } }, "node_modules/@mui/utils": { - "version": "5.11.11", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.11.11.tgz", - "integrity": "sha512-neMM5rrEXYQrOrlxUfns/TGgX4viS8K2zb9pbQh11/oUUYFlGI32Tn+PHePQx7n6Fy/0zq6WxdBFC9VpnJ5JrQ==", + "version": "5.15.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.2.tgz", + "integrity": "sha512-6dGM9/guFKBlFRHA7/mbM+E7wE7CYDy9Ny4JLtD3J+NTyhi8nd8YxlzgAgTaTVqY0BpdQ2zdfB/q6+p2EdGM0w==", "dependencies": { - "@babel/runtime": "^7.21.0", - "@types/prop-types": "^15.7.5", - "@types/react-is": "^16.7.1 || ^17.0.0", + "@babel/runtime": "^7.23.6", + "@types/prop-types": "^15.7.11", "prop-types": "^15.8.1", "react-is": "^18.2.0" }, @@ -2105,10 +2149,120 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers": { + "version": "6.18.6", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.18.6.tgz", + "integrity": "sha512-pqOrGPUDVY/1xXrM1hofqwgquno/SB9aG9CVS1m2Rs8hKF1VWRC+jYlEa1Qk08xKmvkia5g7NsdV/BBb+tHUZw==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@mui/base": "^5.0.0-beta.22", + "@mui/utils": "^5.14.16", + "@types/react-transition-group": "^4.4.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.8.6", + "@mui/system": "^5.8.0", + "date-fns": "^2.25.0", + "date-fns-jalali": "^2.13.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers/node_modules/@mui/base": { + "version": "5.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.29.tgz", + "integrity": "sha512-OXfUssYrB6ch/xpBVHMKAjThPlI9VyGGKdvQLMXef2j39wXfcxPlUVQlwia/lmE3rxWIGvbwkZsDtNYzLMsDUg==", + "dependencies": { + "@babel/runtime": "^7.23.6", + "@floating-ui/react-dom": "^2.0.4", + "@mui/types": "^7.2.11", + "@mui/utils": "^5.15.2", + "@popperjs/core": "^2.11.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers/node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" } }, "node_modules/@next/env": { @@ -2352,9 +2506,9 @@ } }, "node_modules/@popperjs/core": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -3175,9 +3329,9 @@ "dev": true }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/react": { "version": "18.0.25", @@ -3197,18 +3351,10 @@ "@types/react": "*" } }, - "node_modules/@types/react-is": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", - "integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", "dependencies": { "@types/react": "*" } @@ -4917,6 +5063,21 @@ "node": ">=12" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -10825,9 +10986,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", @@ -12630,11 +12791,11 @@ } }, "@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz", + "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==", "requires": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" } }, "@babel/template": { @@ -12680,6 +12841,19 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@date-io/core": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.17.0.tgz", + "integrity": "sha512-+EQE8xZhRM/hsY0CDTVyayMDDY5ihc4MqXCrPxooKw19yAzUIC6uUqsZeaOFNL9YKTNxYKrJP5DFgE8o5xRCOw==" + }, + "@date-io/date-fns": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.17.0.tgz", + "integrity": "sha512-L0hWZ/mTpy3Gx/xXJ5tq5CzHo0L7ry6KEO9/w/JWiFWFLZgiNVo3ex92gOl3zmzjHqY/3Ev+5sehAr8UnGLEng==", + "requires": { + "@date-io/core": "^2.17.0" + } + }, "@emotion/babel-plugin": { "version": "11.10.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz", @@ -12822,18 +12996,35 @@ } }, "@floating-ui/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.1.tgz", - "integrity": "sha512-LSqwPZkK3rYfD7GKoIeExXOyYx6Q1O4iqZWwIehDNuv3Dv425FIAE8PRwtAx1imEolFTHgBEcoFHm9MDnYgPCg==" + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.2.tgz", + "integrity": "sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==", + "requires": { + "@floating-ui/utils": "^0.1.3" + } }, "@floating-ui/dom": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.1.tgz", - "integrity": "sha512-Rt45SmRiV8eU+xXSB9t0uMYiQ/ZWGE/jumse2o3i5RGlyvcbqOF4q+1qBnzLE2kZ5JGhq0iMkcGXUKbFe7MpTA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", "requires": { - "@floating-ui/core": "^1.2.1" + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" } }, + "@floating-ui/react-dom": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz", + "integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==", + "requires": { + "@floating-ui/dom": "^1.5.1" + } + }, + "@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + }, "@fortawesome/fontawesome-common-types": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz", @@ -13661,23 +13852,57 @@ } }, "@mui/types": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.3.tgz", - "integrity": "sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==", + "version": "7.2.11", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.11.tgz", + "integrity": "sha512-KWe/QTEsFFlFSH+qRYf3zoFEj3z67s+qAuSnMMg+gFwbxG7P96Hm6g300inQL1Wy///gSRb8juX7Wafvp93m3w==", "requires": {} }, "@mui/utils": { - "version": "5.11.11", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.11.11.tgz", - "integrity": "sha512-neMM5rrEXYQrOrlxUfns/TGgX4viS8K2zb9pbQh11/oUUYFlGI32Tn+PHePQx7n6Fy/0zq6WxdBFC9VpnJ5JrQ==", + "version": "5.15.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.2.tgz", + "integrity": "sha512-6dGM9/guFKBlFRHA7/mbM+E7wE7CYDy9Ny4JLtD3J+NTyhi8nd8YxlzgAgTaTVqY0BpdQ2zdfB/q6+p2EdGM0w==", "requires": { - "@babel/runtime": "^7.21.0", - "@types/prop-types": "^15.7.5", - "@types/react-is": "^16.7.1 || ^17.0.0", + "@babel/runtime": "^7.23.6", + "@types/prop-types": "^15.7.11", "prop-types": "^15.8.1", "react-is": "^18.2.0" } }, + "@mui/x-date-pickers": { + "version": "6.18.6", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.18.6.tgz", + "integrity": "sha512-pqOrGPUDVY/1xXrM1hofqwgquno/SB9aG9CVS1m2Rs8hKF1VWRC+jYlEa1Qk08xKmvkia5g7NsdV/BBb+tHUZw==", + "requires": { + "@babel/runtime": "^7.23.2", + "@mui/base": "^5.0.0-beta.22", + "@mui/utils": "^5.14.16", + "@types/react-transition-group": "^4.4.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "dependencies": { + "@mui/base": { + "version": "5.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.29.tgz", + "integrity": "sha512-OXfUssYrB6ch/xpBVHMKAjThPlI9VyGGKdvQLMXef2j39wXfcxPlUVQlwia/lmE3rxWIGvbwkZsDtNYzLMsDUg==", + "requires": { + "@babel/runtime": "^7.23.6", + "@floating-ui/react-dom": "^2.0.4", + "@mui/types": "^7.2.11", + "@mui/utils": "^5.15.2", + "@popperjs/core": "^2.11.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1" + } + }, + "clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==" + } + } + }, "@next/env": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.0.2.tgz", @@ -13793,9 +14018,9 @@ } }, "@popperjs/core": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==" + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, "@remix-run/router": { "version": "1.3.2", @@ -14459,9 +14684,9 @@ "dev": true }, "@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "@types/react": { "version": "18.0.25", @@ -14481,18 +14706,10 @@ "@types/react": "*" } }, - "@types/react-is": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", - "integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==", - "requires": { - "@types/react": "*" - } - }, "@types/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", "requires": { "@types/react": "*" } @@ -15725,6 +15942,14 @@ } } }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -20008,9 +20233,9 @@ } }, "regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "regexp.prototype.flags": { "version": "1.4.3", diff --git a/package.json b/package.json index 85d1b24e..39f5377e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@amplitude/analytics-browser": "^1.9.4", + "@date-io/date-fns": "^2.17.0", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@fortawesome/fontawesome-svg-core": "^6.2.0", @@ -23,6 +24,7 @@ "@hassanmojab/react-modern-calendar-datepicker": "^3.1.7", "@mui/lab": "^5.0.0-alpha.121", "@mui/material": "^5.10.13", + "@mui/x-date-pickers": "^6.18.6", "@sentry/nextjs": "^7.50.0", "@types/node": "18.11.9", "@types/react": "18.0.25", @@ -34,6 +36,7 @@ "axios": "^1.2.2", "clsx": "^1.2.1", "d3-force": "^3.0.0", + "date-fns": "^2.30.0", "eslint": "8.27.0", "eslint-config-next": "13.0.2", "eslint-config-prettier": "^8.6.0", diff --git a/src/components/announcements/create/TcPrivateMessaageContainer/TcPrivateMessaageContainer.tsx b/src/components/announcements/create/TcPrivateMessaageContainer/TcPrivateMessaageContainer.tsx deleted file mode 100644 index 516a7eaa..00000000 --- a/src/components/announcements/create/TcPrivateMessaageContainer/TcPrivateMessaageContainer.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React, { useState } from 'react'; -import TcText from '../../../shared/TcText'; -import { MdOutlineAnnouncement } from 'react-icons/md'; -import TcIconContainer from '../TcIconContainer'; -import TcButton from '../../../shared/TcButton'; -import TcSelect from '../../../shared/TcSelect'; -import { FormControl, InputLabel, SelectChangeEvent } from '@mui/material'; -import TcInput from '../../../shared/TcInput'; - -const mockPublicChannels = [ - { - label: 'test', - value: 1, - }, - { - label: 'test2', - value: 2, - }, -]; - -function TcPrivateMessaageContainer() { - const [selectedChannels, setSelectedChannels] = useState([]); - const [message, setMessage] = useState(''); - - const handleSelectChange = (event: SelectChangeEvent) => { - setSelectedChannels(event.target.value as number[]); - }; - - const handleChange = (event: React.ChangeEvent) => { - setMessage(event.target.value); - }; - - return ( -
-
-
- - - - -
- -
-
- - Select Channels - - (selected as number[]) - .map( - (value) => - mockPublicChannels.find( - (channel) => channel.value === value - )?.label - ) - .join(', ') - } - onChange={(event) => handleSelectChange(event)} - /> - - - - -
-
- ); -} - -export default TcPrivateMessaageContainer; diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.spec.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.spec.tsx new file mode 100644 index 00000000..e4827f8d --- /dev/null +++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.spec.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import TcPrivateMessageContainer from './TcPrivateMessaageContainer'; + +describe('TcPrivateMessageContainer Tests', () => { + test('renders the component without crashing', () => { + render(); + expect(screen.getByText('Private Message (optional)')).toBeInTheDocument(); + }); + + test('initial states are set correctly', () => { + render(); + }); + + test('toggles private message switch', () => { + render(); + const switchControl = screen.getByRole('checkbox'); + fireEvent.click(switchControl); + }); + + test('message type buttons respond to clicks', () => { + render(); + }); + + test('allows the user to enter a message', async () => { + render(); + + const privateMessageToggle = screen.getByRole('checkbox'); + fireEvent.click(privateMessageToggle); + + const messageInput = (await screen.findByPlaceholderText( + 'Write your message here' + )) as HTMLInputElement; + fireEvent.change(messageInput, { target: { value: 'Test Message' } }); + + expect(messageInput.value).toBe('Test Message'); + }); + + test('handles channel and username selection based on message type', () => { + render(); + }); +}); diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx new file mode 100644 index 00000000..a272e31c --- /dev/null +++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx @@ -0,0 +1,195 @@ +import React, { useState } from 'react'; +import TcText from '../../../shared/TcText'; +import { MdOutlineAnnouncement } from 'react-icons/md'; +import TcIconContainer from '../TcIconContainer'; +import TcButton from '../../../shared/TcButton'; +import TcSelect from '../../../shared/TcSelect'; +import { + FormControl, + FormControlLabel, + InputLabel, + SelectChangeEvent, +} from '@mui/material'; +import TcInput from '../../../shared/TcInput'; +import TcSwitch from '../../../shared/TcSwitch'; +import TcIconWithTooltip from '../../../shared/TcIconWithTooltip'; +import TcButtonGroup from '../../../shared/TcButtonGroup'; +import clsx from 'clsx'; + +const mockPublicChannels = [ + { + label: 'test', + value: 1, + }, + { + label: 'test2', + value: 2, + }, +]; + +export enum MessageType { + Both = 'Both', + RoleOnly = 'Role Only', + UserOnly = 'User Only', +} + +function TcPrivateMessageContainer() { + const [privateMessage, setPrivateMessage] = useState(false); + const [messageType, setMessageType] = useState(MessageType.Both); + const [selectedChannels, setSelectedChannels] = useState([]); + const [message, setMessage] = useState(''); + + const handleSelectChange = (event: SelectChangeEvent) => { + setSelectedChannels(event.target.value as number[]); + }; + + const handleChange = (event: React.ChangeEvent) => { + setMessage(event.target.value); + }; + + const handlePrivateMessageChange = ( + event: React.ChangeEvent + ) => { + setPrivateMessage(event.target.checked); + }; + + const messageTypesArray = Object.values(MessageType); + + return ( +
+
+
+ + + + + } + label={ +
+ + +
+ } + /> +
+
+ + {messageTypesArray.map((el) => ( + setMessageType(el)} + /> + ))} + + +
+
+ {privateMessage && ( +
+
+ + Select Role(s) + + (selected as number[]) + .map( + (value) => + mockPublicChannels.find( + (channel) => channel.value === value + )?.label + ) + .join(', ') + } + onChange={(event) => handleSelectChange(event)} + /> + + + + Select Username(s) + + + (selected as number[]) + .map( + (value) => + mockPublicChannels.find( + (channel) => channel.value === value + )?.label + ) + .join(', ') + } + onChange={(event) => handleSelectChange(event)} + /> + +
+ + + +
+ )} +
+ ); +} + +export default TcPrivateMessageContainer; diff --git a/src/components/announcements/create/TcPrivateMessaageContainer/index.ts b/src/components/announcements/create/privateMessaageContainer/index.ts similarity index 100% rename from src/components/announcements/create/TcPrivateMessaageContainer/index.ts rename to src/components/announcements/create/privateMessaageContainer/index.ts diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.spec.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.spec.tsx new file mode 100644 index 00000000..50a4c3c9 --- /dev/null +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.spec.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import TcPublicMessaageContainer from './TcPublicMessaageContainer'; + +describe('TcPublicMessaageContainer Tests', () => { + test('renders the component without crashing', () => { + render(); + expect(screen.getByText('Public Message')).toBeInTheDocument(); + }); + + test('initial state is set correctly', () => { + render(); + expect(screen.getByPlaceholderText('Write your message here')).toHaveValue( + '' + ); + }); + + test('allows the user to enter a message', () => { + render(); + const messageInput = screen.getByPlaceholderText( + 'Write your message here' + ) as HTMLInputElement; + fireEvent.change(messageInput, { target: { value: 'Test Message' } }); + expect(messageInput.value).toBe('Test Message'); + }); + + test('select channels dropdown is rendered', () => { + render(); + expect(screen.getByLabelText('Select Channels')).toBeInTheDocument(); + }); +}); diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx index 013ef562..5c851641 100644 --- a/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx @@ -4,7 +4,12 @@ import { MdAnnouncement } from 'react-icons/md'; import TcIconContainer from '../TcIconContainer'; import TcButton from '../../../shared/TcButton'; import TcSelect from '../../../shared/TcSelect'; -import { FormControl, InputLabel, SelectChangeEvent } from '@mui/material'; +import { + FormControl, + FormHelperText, + InputLabel, + SelectChangeEvent, +} from '@mui/material'; import TcInput from '../../../shared/TcInput'; const mockPublicChannels = [ @@ -39,7 +44,11 @@ function TcPublicMessaageContainer() { - +
@@ -63,6 +72,10 @@ function TcPublicMessaageContainer() { } onChange={(event) => handleSelectChange(event)} /> + + The announcement will be sent by the a bot which will have access to + send the following message within the selected channels + void; + selectedDate: Date | null; + handleDateChange: (date: Date | null) => void; + selectedTime: Date | null; + handleTimeChange: (time: Date | null) => void; + activeTab: number; + setActiveTab: React.Dispatch>; +} + +function TcDateTimePopover({ + open, + anchorEl, + onClose, + selectedDate, + handleDateChange, + selectedTime, + handleTimeChange, + activeTab, + setActiveTab, +}: IDateTimePopoverProps) { + const disablePastDates = (date: Date): boolean => { + const today = new Date(); + today.setHours(0, 0, 0, 0); + return date < today; + }; + + const tabContent = [ + + + , + + + , + ]; + + return ( + + {tabContent[activeTab]} + + setActiveTab(newValue)} + indicatorColor="secondary" + className="w-full border-t border-gray-200" + > + } + className="w-1/2" + data-testid="calendar-icon" + /> + } + className="w-1/2" + data-testid="time-icon" + /> + +
+ } + /> + ); +} + +export default TcDateTimePopover; diff --git a/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.spec.tsx b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.spec.tsx new file mode 100644 index 00000000..870106c8 --- /dev/null +++ b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.spec.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import TcScheduleAnnouncement from './TcScheduleAnnouncement'; + +describe('TcScheduleAnnouncement Tests', () => { + test('renders the component without crashing', () => { + render(); + expect(screen.getByText('Schedule Announcement')).toBeInTheDocument(); + }); +}); diff --git a/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx new file mode 100644 index 00000000..3eed512e --- /dev/null +++ b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx @@ -0,0 +1,90 @@ +import React, { useState } from 'react'; +import TcIconContainer from '../TcIconContainer'; +import { MdCalendarMonth } from 'react-icons/md'; +import TcText from '../../../shared/TcText'; +import TcButton from '../../../shared/TcButton'; +import moment from 'moment'; +import TcTcDateTimePopover from './TcDateTimePopover'; + +function TcScheduleAnnouncement() { + const [anchorEl, setAnchorEl] = useState(null); + const [activeTab, setActiveTab] = useState(0); + const [selectedDate, setSelectedDate] = useState(null); + const [selectedTime, setSelectedTime] = useState(null); + const [dateTimeDisplay, setDateTimeDisplay] = useState( + moment().format('D MMMM YYYY @ h A') + ); + + const handleOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + const id = open ? 'date-time-popover' : undefined; + + const handleDateChange = (date: Date | null) => { + if (date) { + setSelectedDate(date); + setActiveTab(1); + } + }; + + const handleTimeChange = (time: Date | null) => { + if (time) { + setSelectedTime(time); + handleClose(); + + if (selectedDate) { + const fullDateTime = moment(selectedDate).set({ + hour: time.getHours(), + minute: time.getMinutes(), + }); + setDateTimeDisplay(fullDateTime.format('D MMMM YYYY @ h A')); + } + } + }; + + return ( +
+
+
+ + + + +
+ } + disableElevation={true} + className="border border-black bg-gray-100 shadow-md" + sx={{ color: 'black', height: '2.4rem' }} + aria-describedby={id} + onClick={handleOpen} + /> + +
+
+ ); +} + +export default TcScheduleAnnouncement; diff --git a/src/components/announcements/create/scheduleAnnouncement/index.ts b/src/components/announcements/create/scheduleAnnouncement/index.ts new file mode 100644 index 00000000..143415a6 --- /dev/null +++ b/src/components/announcements/create/scheduleAnnouncement/index.ts @@ -0,0 +1,3 @@ +import { default as TcScheduleAnnouncement } from './TcScheduleAnnouncement'; + +export default TcScheduleAnnouncement; diff --git a/src/components/announcements/create/selectPlatform/TcSelectPlatform.tsx b/src/components/announcements/create/selectPlatform/TcSelectPlatform.tsx new file mode 100644 index 00000000..56a5e615 --- /dev/null +++ b/src/components/announcements/create/selectPlatform/TcSelectPlatform.tsx @@ -0,0 +1,47 @@ +import { FormControl, InputLabel } from '@mui/material'; +import React from 'react'; +import TcSelect from '../../../shared/TcSelect'; +import TcText from '../../../shared/TcText'; +import { BsDiscord, BsTelegram } from 'react-icons/bs'; + +const announcementsPlatforms = [ + { + label: 'Discord', + value: '1', + icon: , + }, + { + label: 'Telegram(TBA)', + value: '2', + disabled: true, + icon: , + }, +]; + +function TcSelectPlatform() { + return ( +
+
+ + +
+ + Select Platform + + +
+ ); +} + +export default TcSelectPlatform; diff --git a/src/components/announcements/create/selectPlatform/index.ts b/src/components/announcements/create/selectPlatform/index.ts new file mode 100644 index 00000000..802608b8 --- /dev/null +++ b/src/components/announcements/create/selectPlatform/index.ts @@ -0,0 +1,3 @@ +import { default as TcSelectPlatform } from './TcSelectPlatform'; + +export default TcSelectPlatform; diff --git a/src/components/shared/TcBreadcrumbs.tsx b/src/components/shared/TcBreadcrumbs.tsx index 465b792b..526d32ab 100644 --- a/src/components/shared/TcBreadcrumbs.tsx +++ b/src/components/shared/TcBreadcrumbs.tsx @@ -1,37 +1,15 @@ -/** - * `TcBreadcrumbs` Component - * - * This component is used for displaying a breadcrumb navigation interface. It is built - * using Material-UI's `Breadcrumbs` and a custom `TcLink` component for navigation. - * - * Props: - * - `items`: An array of `BreadcrumbItem` objects. Each `BreadcrumbItem` should have: - * - `label` (string): The text displayed for the breadcrumb link. - * - `path` (string): The navigation path the breadcrumb link points to. - * - * Usage: - * - * - * This component renders breadcrumbs for the provided `items` array. Each item in the array - * represents a single breadcrumb link. The component uses flexbox for alignment and spacing, - * and includes a hover effect on the links for better user interaction. - */ - import React from 'react'; import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Link from '@mui/material/Link'; import { useRouter } from 'next/router'; import TcLink from './TcLink'; -import { MdOutlineKeyboardArrowLeft } from 'react-icons/md'; +import { ArrowDropDownIcon } from '@mui/x-date-pickers'; +import { MdChevronRight } from 'react-icons/md'; +import TcText from './TcText'; interface BreadcrumbItem { label: string; - path: string; + path?: string; } interface TcBreadcrumbsProps { @@ -50,22 +28,25 @@ function TcBreadcrumbs({ items }: TcBreadcrumbsProps) { }; return ( - - {items.map((item) => ( -
} + > + {items.map((item, index) => ( + handleClick(event, item.path || '')} + underline={'none'} + className={`${ + index === items.length - 1 + ? 'pointer-events-none text-black' + : 'text-gray-500' + }`} + to={item.path || '#'} > - - handleClick(event, item.path)} - > - {item.label} - -
+ + ))}
); diff --git a/src/components/shared/TcLink.tsx b/src/components/shared/TcLink.tsx index 192004b5..c7b2d7a3 100644 --- a/src/components/shared/TcLink.tsx +++ b/src/components/shared/TcLink.tsx @@ -22,7 +22,7 @@ import React from 'react'; import { Link, LinkProps as MuiLinkProps } from '@mui/material'; interface CustomLinkProps extends MuiLinkProps { - to: string; + to?: string; } function TcLink({ children, to, ...props }: CustomLinkProps) { diff --git a/src/components/shared/TcTabs/TcTab/TcTab.tsx b/src/components/shared/TcTabs/TcTab/TcTab.tsx new file mode 100644 index 00000000..e05b7b2e --- /dev/null +++ b/src/components/shared/TcTabs/TcTab/TcTab.tsx @@ -0,0 +1,10 @@ +import { Tab, TabProps } from '@mui/material'; +import React from 'react'; + +interface ITcTabProps extends TabProps {} + +function TcTab({ ...props }: ITcTabProps) { + return ; +} + +export default TcTab; diff --git a/src/components/shared/TcTabs/TcTab/index.ts b/src/components/shared/TcTabs/TcTab/index.ts new file mode 100644 index 00000000..da866174 --- /dev/null +++ b/src/components/shared/TcTabs/TcTab/index.ts @@ -0,0 +1,3 @@ +import { default as TcTab } from './TcTab'; + +export default TcTab; diff --git a/src/components/shared/TcTabs/TcTabs.tsx b/src/components/shared/TcTabs/TcTabs.tsx new file mode 100644 index 00000000..46b53e53 --- /dev/null +++ b/src/components/shared/TcTabs/TcTabs.tsx @@ -0,0 +1,26 @@ +import { Tabs, TabsProps } from '@mui/material'; +import React from 'react'; + +interface ITcTabsProps extends TabsProps { + children: React.ReactElement | React.ReactElement[]; +} + +/** + * `TcTabs` is a functional React component that renders Material-UI's `Tabs` component + * along with any child components passed to it. This component allows for the standard + * functionality of MUI's `Tabs` while also enabling the insertion of `Tab` components + * or other custom elements as children. + * + * @param {ITcTabsProps} props - Includes standard properties of MUI's `Tabs` component + * and any additional props defined in `ITcTabsProps`. The `children` prop is explicitly + * typed to accept either a single React element or an array of React elements, which are + * typically `Tab` components. + * + * @returns {React.ReactElement} - A `Tabs` component from Material-UI, rendering the passed + * children within. + */ +function TcTabs({ children, ...props }: ITcTabsProps): React.ReactElement { + return {children}; +} + +export default TcTabs; diff --git a/src/components/shared/TcTabs/index.ts b/src/components/shared/TcTabs/index.ts new file mode 100644 index 00000000..6dd42fa9 --- /dev/null +++ b/src/components/shared/TcTabs/index.ts @@ -0,0 +1,3 @@ +import { default as TcTabs } from './TcTabs'; + +export default TcTabs; diff --git a/src/pages/announcements/create-new-announcements.tsx b/src/pages/announcements/create-new-announcements.tsx index f42b9be9..c5e70818 100644 --- a/src/pages/announcements/create-new-announcements.tsx +++ b/src/pages/announcements/create-new-announcements.tsx @@ -2,64 +2,52 @@ import React from 'react'; import { defaultLayout } from '../../layouts/defaultLayout'; import SEO from '../../components/global/SEO'; import TcBoxContainer from '../../components/shared/TcBox/TcBoxContainer'; -import TcText from '../../components/shared/TcText'; -import TcSelect from '../../components/shared/TcSelect'; -import { FormControl, InputLabel } from '@mui/material'; -import { BsDiscord, BsTelegram } from 'react-icons/bs'; import TcPublicMessaageContainer from '../../components/announcements/create/publicMessageContainer'; -import TcPrivateMessaageContainer from '../../components/announcements/create/TcPrivateMessaageContainer'; - -const announcementsPlatforms = [ - { - label: 'Discord', - value: '1', - icon: , - }, - { - label: 'Telegram(TBA)', - value: '2', - disabled: true, - icon: , - }, -]; +import TcPrivateMessaageContainer from '../../components/announcements/create/privateMessaageContainer'; +import TcButton from '../../components/shared/TcButton'; +import TcScheduleAnnouncement from '../../components/announcements/create/scheduleAnnouncement/'; +import TcSelectPlatform from '../../components/announcements/create/selectPlatform'; +import TcBreadcrumbs from '../../components/shared/TcBreadcrumbs'; function CreateNewAnnouncements() { return ( <>
+ -
-
- +
+ + + + +
+
+ +
+ -
- - - Select Platform - - -
- -
} /> diff --git a/src/pages/community-settings/platform/index.tsx b/src/pages/community-settings/platform/index.tsx index 716703ef..6c341877 100644 --- a/src/pages/community-settings/platform/index.tsx +++ b/src/pages/community-settings/platform/index.tsx @@ -14,6 +14,7 @@ function Index() { diff --git a/src/utils/theme.ts b/src/utils/theme.ts index 38e0d8b1..9e96cf3d 100644 --- a/src/utils/theme.ts +++ b/src/utils/theme.ts @@ -110,29 +110,29 @@ export const theme = createTheme({ }, MuiTab: { styleOverrides: { - root: { - textTransform: 'none', - borderRadius: '10px 10px 0 0', - padding: '8px 24px', - width: '214px', - height: '40px', - gap: '10px', - borderBottom: 'none', - '&.Mui-selected': { - background: '#804EE1', - color: 'white', - border: 0, - borderBottom: 'none', - }, - '&$selected': { - borderBottom: 'none', - }, - '&:not(.Mui-selected)': { - backgroundColor: '#EDEDED', - color: '#222222', - }, - selected: {}, - }, + // root: { + // textTransform: 'none', + // borderRadius: '10px 10px 0 0', + // padding: '8px 24px', + // width: '214px', + // height: '40px', + // gap: '10px', + // borderBottom: 'none', + // '&.Mui-selected': { + // background: '#804EE1', + // color: 'white', + // border: 0, + // borderBottom: 'none', + // }, + // '&$selected': { + // borderBottom: 'none', + // }, + // '&:not(.Mui-selected)': { + // backgroundColor: '#EDEDED', + // color: '#222222', + // }, + // selected: {}, + // }, }, }, }, From fd9db254f632e8aae5d5d45dd157b10453dfab7d Mon Sep 17 00:00:00 2001 From: zuies Date: Thu, 4 Jan 2024 16:05:52 +0300 Subject: [PATCH 13/30] update announcements-list page --- .../announcements/TcTimeZone.spec.tsx | 17 ++ src/components/announcements/TcTimeZone.tsx | 122 ++++++++++++++ .../TcScheduleAnnouncement.tsx | 4 +- .../shared/TcPagination/TcPagination.spec.tsx | 42 +++++ .../shared/TcPagination/TcPagination.tsx | 60 +++++++ src/components/shared/TcPagination/index.ts | 3 + .../shared/TcTableContainer/TcTableHead.tsx | 5 +- .../shared/TcTableContainer/TcTableRow.tsx | 15 +- src/pages/announcements/index.tsx | 158 ++++++++++++++++-- 9 files changed, 404 insertions(+), 22 deletions(-) create mode 100644 src/components/announcements/TcTimeZone.spec.tsx create mode 100644 src/components/announcements/TcTimeZone.tsx create mode 100644 src/components/shared/TcPagination/TcPagination.spec.tsx create mode 100644 src/components/shared/TcPagination/TcPagination.tsx create mode 100644 src/components/shared/TcPagination/index.ts diff --git a/src/components/announcements/TcTimeZone.spec.tsx b/src/components/announcements/TcTimeZone.spec.tsx new file mode 100644 index 00000000..b30eaca2 --- /dev/null +++ b/src/components/announcements/TcTimeZone.spec.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import TcTimeZone from './TcTimeZone'; + +describe('TcTimeZone', () => { + test('renders TcTimeZone component', () => { + render(); + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + test('opens popover on button click', () => { + render(); + const button = screen.getByRole('button'); + fireEvent.click(button); + expect(screen.getByText('Search timezone')).toBeInTheDocument(); + }); +}); diff --git a/src/components/announcements/TcTimeZone.tsx b/src/components/announcements/TcTimeZone.tsx new file mode 100644 index 00000000..517826e1 --- /dev/null +++ b/src/components/announcements/TcTimeZone.tsx @@ -0,0 +1,122 @@ +import React, { useState } from 'react'; +import TcButton from '../shared/TcButton'; +import { FaGlobeAmericas } from 'react-icons/fa'; +import TcPopover from '../shared/TcPopover'; + +import momentTZ from 'moment-timezone'; +import moment from 'moment'; +import 'moment-timezone'; +import TcInput from '../shared/TcInput'; +import { InputAdornment } from '@mui/material'; +import { MdSearch } from 'react-icons/md'; + +const timeZonesList = momentTZ.tz.names(); + +function TcTimeZone() { + const [activeZone, setActiveZone] = useState(moment.tz.guess()); + + const [anchorEl, setAnchorEl] = useState(null); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + setZones(timeZonesList); + }; + + const open = Boolean(anchorEl); + const id = open ? 'simple-popover' : undefined; + + const [zones, setZones] = useState(timeZonesList); + + const searchZones = (e: { target: { value: string } }) => { + const results = timeZonesList.filter((zone) => { + if (e.target.value === '') { + return timeZonesList; + } + return zone.toLowerCase().includes(e.target.value.toLowerCase()); + }); + setZones(results); + }; + + const handleTimeZoneSelect = (timeZone: string) => { + setActiveZone(timeZone); + setAnchorEl(null); + setZones(timeZonesList); + }; + + return ( +
+ } + aria-describedby={id} + onClick={handleClick} + /> + +
+ + + + ), + }} + onChange={searchZones} + /> +
+
    + {zones.length > 0 ? ( + zones.map((el) => ( +
  • handleTimeZoneSelect(el)} + > +
    {el}
    +
  • + )) + ) : ( +
    + Not founded +
    + )} +
+
+ } + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'center', + }} + /> +
+ ); +} + +export default TcTimeZone; diff --git a/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx index 3eed512e..bc3594fd 100644 --- a/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx +++ b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx @@ -4,7 +4,7 @@ import { MdCalendarMonth } from 'react-icons/md'; import TcText from '../../../shared/TcText'; import TcButton from '../../../shared/TcButton'; import moment from 'moment'; -import TcTcDateTimePopover from './TcDateTimePopover'; +import TcDateTimePopover from './TcDateTimePopover'; function TcScheduleAnnouncement() { const [anchorEl, setAnchorEl] = useState(null); @@ -71,7 +71,7 @@ function TcScheduleAnnouncement() { aria-describedby={id} onClick={handleOpen} /> - { + const totalItems = 100; + const itemsPerPage = 10; + const currentPage = 1; + const onChangePage = jest.fn(); + + it('renders the pagination component correctly', () => { + const { getByText } = render( + + ); + + // Ensure the pagination component renders with the correct total pages and current page. + expect(getByText('1')).toBeInTheDocument(); + expect(getByText('10')).toBeInTheDocument(); + }); + + it('calls onChangePage when a page is clicked', () => { + const { getByText } = render( + + ); + + // Click on page 2 + fireEvent.click(getByText('2')); + + // Ensure onChangePage is called with the correct page number (2) + expect(onChangePage).toHaveBeenCalledWith(2); + }); +}); diff --git a/src/components/shared/TcPagination/TcPagination.tsx b/src/components/shared/TcPagination/TcPagination.tsx new file mode 100644 index 00000000..8a95a1d2 --- /dev/null +++ b/src/components/shared/TcPagination/TcPagination.tsx @@ -0,0 +1,60 @@ +import { Pagination, PaginationItem, PaginationProps } from '@mui/material'; + +interface ITcPaginationProps extends PaginationProps { + totalItems: number; + itemsPerPage: number; + currentPage: number; + onChangePage: (page: number) => void; +} + +/** + * TcPagination Component + * + * A pagination component using Material-UI's `Pagination` to handle page navigation. + * + * @component + * @param {ITcPaginationProps} props - The props for configuring the pagination. + * @param {number} props.totalItems - The total number of items to paginate. + * @param {number} props.itemsPerPage - The number of items per page. + * @param {number} props.currentPage - The current active page. + * @param {(page: number) => void} props.onChangePage - A callback function to handle page changes. + * @returns {JSX.Element} - The rendered pagination component. + * + * @example + * // Usage: + * handlePageChange(page)} + * /> + */ + +function TcPagination({ + onChangePage, + currentPage, + itemsPerPage, + totalItems, + ...props +}: ITcPaginationProps): JSX.Element { + const totalPages = Math.ceil(totalItems / itemsPerPage); + + const handleChangePage = (page: number) => { + if (page !== currentPage) { + onChangePage(page); + } + }; + + return ( + handleChangePage(page)} + {...props} + renderItem={(item) => } + /> + ); +} + +export default TcPagination; diff --git a/src/components/shared/TcPagination/index.ts b/src/components/shared/TcPagination/index.ts new file mode 100644 index 00000000..8995d5b0 --- /dev/null +++ b/src/components/shared/TcPagination/index.ts @@ -0,0 +1,3 @@ +import { default as TcPagination } from './TcPagination'; + +export default TcPagination; diff --git a/src/components/shared/TcTableContainer/TcTableHead.tsx b/src/components/shared/TcTableContainer/TcTableHead.tsx index ef6cc01b..f301c81e 100644 --- a/src/components/shared/TcTableContainer/TcTableHead.tsx +++ b/src/components/shared/TcTableContainer/TcTableHead.tsx @@ -15,7 +15,10 @@ interface ITcTableHeadProps extends TableHeadProps { function TcTableHead({ headers, ...props }: ITcTableHeadProps) { return ( - + ); } diff --git a/src/components/shared/TcTableContainer/TcTableRow.tsx b/src/components/shared/TcTableContainer/TcTableRow.tsx index ebe64525..bcadf15e 100644 --- a/src/components/shared/TcTableContainer/TcTableRow.tsx +++ b/src/components/shared/TcTableContainer/TcTableRow.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { TableRow, TableRowProps } from '@mui/material'; import TcTableCell from './TcTableCell'; +import clsx from 'clsx'; interface ITcTableRowProps extends TableRowProps { rowItem: { [key: string]: any }; + customTableCellClasses?: string; customRenderers?: { [key: string]: (value: any) => React.ReactNode }; } @@ -13,7 +15,12 @@ interface ITcTableRowProps extends TableRowProps { * @param {ITcTableRowProps} props - The component props. */ -function TcTableRow({ rowItem, customRenderers, ...props }: ITcTableRowProps) { +function TcTableRow({ + rowItem, + customRenderers, + customTableCellClasses, + ...props +}: ITcTableRowProps) { return ( {rowItem && @@ -22,7 +29,11 @@ function TcTableRow({ rowItem, customRenderers, ...props }: ITcTableRowProps) { return ( {CustomRenderer ? CustomRenderer(value) : value} diff --git a/src/pages/announcements/index.tsx b/src/pages/announcements/index.tsx index cb74d1ea..4467bd5d 100644 --- a/src/pages/announcements/index.tsx +++ b/src/pages/announcements/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { defaultLayout } from '../../layouts/defaultLayout'; import TcBoxContainer from '../../components/shared/TcBox/TcBoxContainer'; import SEO from '../../components/global/SEO'; @@ -7,33 +7,157 @@ import TcButton from '../../components/shared/TcButton'; import { BsPlus } from 'react-icons/bs'; import TcTableContainer from '../../components/shared/TcTableContainer'; import router from 'next/router'; +import TcPagination from '../../components/shared/TcPagination'; +import TcTimeZone from '../../components/announcements/TcTimeZone'; +import TcDateTimePopover from '../../components/announcements/create/scheduleAnnouncement/TcDateTimePopover'; +import moment from 'moment'; +import { MdCalendarMonth } from 'react-icons/md'; -const bodyRowItems = [ - { Name: 'Alice', Age: 28, Location: 'New York' }, - { Name: 'Bob', Age: 34, Location: 'San Francisco' }, - { Name: 'Carol', Age: 23, Location: 'Miami' }, +const headers = ['Announcement', 'Channel', 'Handle', 'Role', 'Date']; +const bodyRowItems: any[] = [ + // { + // Announcement: 'Lorem Ipsum Announcement', + // Channel: 'General', + // Handle: 'JohnDoe', + // Role: 'Admin', + // Date: '2023-03-15', + // }, + // { + // Announcement: 'New Feature Release', + // Channel: 'Product Updates', + // Handle: 'JaneSmith', + // Role: 'User', + // Date: '2023-03-20', + // }, + // { + // Announcement: 'Upcoming Event', + // Channel: 'Events', + // Handle: 'EventHost', + // Role: 'Moderator', + // Date: '2023-04-05', + // }, ]; function Index() { + const [anchorEl, setAnchorEl] = useState(null); + const [activeTab, setActiveTab] = useState(0); + const [selectedDate, setSelectedDate] = useState(null); + const [selectedTime, setSelectedTime] = useState(null); + const [dateTimeDisplay, setDateTimeDisplay] = useState( + moment().format('D MMMM YYYY @ h A') + ); + + const handleOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + const id = open ? 'date-time-popover' : undefined; + + const handleDateChange = (date: Date | null) => { + if (date) { + setSelectedDate(date); + setActiveTab(1); + } + }; + + const handleTimeChange = (time: Date | null) => { + if (time) { + setSelectedTime(time); + handleClose(); + + if (selectedDate) { + const fullDateTime = moment(selectedDate).set({ + hour: time.getHours(), + minute: time.getMinutes(), + }); + setDateTimeDisplay(fullDateTime.format('D MMMM YYYY @ h A')); + } + } + }; + return ( <>
-
- - } - variant="outlined" - onClick={() => - router.push('/announcements/create-new-announcements') - } - /> +
+
+
+ + } + variant="outlined" + onClick={() => + router.push('/announcements/create-new-announcements') + } + /> +
+
+ } + disableElevation={true} + className="border border-black bg-gray-100 shadow-md" + sx={{ color: 'black', height: '2.4rem' }} + aria-describedby={id} + onClick={handleOpen} + /> + + +
+ {bodyRowItems.length > 0 ? ( + + ) : ( +
+ + +
+ )} +
+ +
+ {bodyRowItems.length > 0 ? ( + + ) : ( + '' + )}
-
} /> From bdcf53a2bf58a1f1437042339d69d34a6cfcf81a Mon Sep 17 00:00:00 2001 From: zuies Date: Thu, 4 Jan 2024 16:11:52 +0300 Subject: [PATCH 14/30] make announcements-list page responsive --- src/components/announcements/TcTimeZone.tsx | 2 +- src/pages/announcements/index.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/announcements/TcTimeZone.tsx b/src/components/announcements/TcTimeZone.tsx index 517826e1..d37e357a 100644 --- a/src/components/announcements/TcTimeZone.tsx +++ b/src/components/announcements/TcTimeZone.tsx @@ -48,7 +48,7 @@ function TcTimeZone() { }; return ( -
+
-
+
-
+
) : ( -
+
Date: Thu, 4 Jan 2024 16:53:56 +0300 Subject: [PATCH 15/30] make create announcements page responsive --- .../TcPrivateMessaageContainer.tsx | 30 +++++++++++++------ .../TcPublicMessaageContainer.tsx | 12 ++++++-- .../TcScheduleAnnouncement.tsx | 2 +- .../selectPlatform/TcSelectPlatform.tsx | 3 +- .../create-new-announcements.tsx | 25 ++++++++++++---- 5 files changed, 53 insertions(+), 19 deletions(-) diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx index a272e31c..53141205 100644 --- a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx +++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx @@ -57,15 +57,14 @@ function TcPrivateMessageContainer() { return (
-
-
+
+
- } + className="mx-auto md:mx-0" + control={} label={
@@ -78,8 +77,12 @@ function TcPrivateMessageContainer() { } />
-
- +
+ {messageTypesArray.map((el) => ( setMessageType(el)} /> @@ -101,7 +107,13 @@ function TcPrivateMessageContainer() {
diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx index 5c851641..bfe725b5 100644 --- a/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx @@ -37,8 +37,8 @@ function TcPublicMessaageContainer() { return (
-
-
+
+
@@ -47,7 +47,13 @@ function TcPublicMessaageContainer() {
diff --git a/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx index bc3594fd..ab71c975 100644 --- a/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx +++ b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx @@ -50,7 +50,7 @@ function TcScheduleAnnouncement() { return (
-
+
diff --git a/src/components/announcements/create/selectPlatform/TcSelectPlatform.tsx b/src/components/announcements/create/selectPlatform/TcSelectPlatform.tsx index 56a5e615..35608a87 100644 --- a/src/components/announcements/create/selectPlatform/TcSelectPlatform.tsx +++ b/src/components/announcements/create/selectPlatform/TcSelectPlatform.tsx @@ -20,12 +20,13 @@ const announcementsPlatforms = [ function TcSelectPlatform() { return ( -
+
diff --git a/src/pages/announcements/create-new-announcements.tsx b/src/pages/announcements/create-new-announcements.tsx index c5e70818..2820464e 100644 --- a/src/pages/announcements/create-new-announcements.tsx +++ b/src/pages/announcements/create-new-announcements.tsx @@ -29,22 +29,37 @@ function CreateNewAnnouncements() {
-
+
-
+
From 3ab8322c63fb225e67d29cdb14d577de07c3e6b1 Mon Sep 17 00:00:00 2001 From: zuies Date: Thu, 4 Jan 2024 17:13:28 +0300 Subject: [PATCH 16/30] add slice for announcements --- .../create-new-announcements.tsx | 2 + src/store/slices/announcementsSlice.ts | 48 +++++++++++++++++++ src/store/types/IAnnouncements.ts | 1 + src/store/useStore.ts | 2 + 4 files changed, 53 insertions(+) create mode 100644 src/store/slices/announcementsSlice.ts create mode 100644 src/store/types/IAnnouncements.ts diff --git a/src/pages/announcements/create-new-announcements.tsx b/src/pages/announcements/create-new-announcements.tsx index 2820464e..992dd003 100644 --- a/src/pages/announcements/create-new-announcements.tsx +++ b/src/pages/announcements/create-new-announcements.tsx @@ -8,6 +8,7 @@ import TcButton from '../../components/shared/TcButton'; import TcScheduleAnnouncement from '../../components/announcements/create/scheduleAnnouncement/'; import TcSelectPlatform from '../../components/announcements/create/selectPlatform'; import TcBreadcrumbs from '../../components/shared/TcBreadcrumbs'; +import router from 'next/router'; function CreateNewAnnouncements() { return ( @@ -32,6 +33,7 @@ function CreateNewAnnouncements() {
router.push('/announcements')} variant="outlined" sx={{ maxWidth: { diff --git a/src/store/slices/announcementsSlice.ts b/src/store/slices/announcementsSlice.ts new file mode 100644 index 00000000..a8adaa5c --- /dev/null +++ b/src/store/slices/announcementsSlice.ts @@ -0,0 +1,48 @@ +import { StateCreator } from 'zustand'; +import { axiosInstance } from '../../axiosInstance'; +import IAnnouncements from '../types/IAnnouncements'; + +const createAnnouncementsSlice: StateCreator = (set, get) => ({ + retrieveAnnouncements: async () => { + try { + const { data } = await axiosInstance.get(`/announcements/`); + return data; + } catch (error) { + console.error('Failed to retrieve announcements:', error); + } + }, + retrieveAnnouncementById: async (id: string) => { + try { + const { data } = await axiosInstance.get(`/announcements/${id}`); + return data; + } catch (error) { + console.error('Failed to retrieve announcement:', error); + } + }, + createNewAnnouncements: async () => { + try { + const { data } = await axiosInstance.post(`/announcements/`); + return data; + } catch (error) { + console.error('Failed to create announcements:', error); + } + }, + patchExistingAnnouncement: async (id: string) => { + try { + const { data } = await axiosInstance.post(`/announcements/${id}`); + return data; + } catch (error) { + console.error('Failed to patch announcements:', error); + } + }, + deleteAnnouncements: async (id: string) => { + try { + const { data } = await axiosInstance.delete(`/platforms/${id}`); + return data; + } catch (error) { + console.error('Failed to delete announcements:', error); + } + }, +}); + +export default createAnnouncementsSlice; diff --git a/src/store/types/IAnnouncements.ts b/src/store/types/IAnnouncements.ts new file mode 100644 index 00000000..7691c89c --- /dev/null +++ b/src/store/types/IAnnouncements.ts @@ -0,0 +1 @@ +export default interface IAnnouncements {} diff --git a/src/store/useStore.ts b/src/store/useStore.ts index 16a674c6..4a6fd939 100644 --- a/src/store/useStore.ts +++ b/src/store/useStore.ts @@ -7,6 +7,7 @@ import twitterSlice from './slices/twitterSlice'; import centricSlice from './slices/centricSlice'; import platformSlice from './slices/platformSlice'; import userSlice from './slices/userSlice'; +import announcementsSlice from './slices/announcementsSlice'; const useAppStore = create()((...a) => ({ ...createChartSlice(...a), @@ -17,6 +18,7 @@ const useAppStore = create()((...a) => ({ ...centricSlice(...a), ...platformSlice(...a), ...userSlice(...a), + ...announcementsSlice(...a), })); export default useAppStore; From fbdb69926690d8f383d7c77e7160c7f4adee28c7 Mon Sep 17 00:00:00 2001 From: zuies Date: Mon, 8 Jan 2024 14:12:09 +0300 Subject: [PATCH 17/30] add dialog for public message --- .../TcPublicMessaageContainer.tsx | 17 ++-- .../TcPublicMessagePreviewDialog.spec.tsx | 83 ++++++++++++++++ .../TcPublicMessagePreviewDialog.tsx | 95 +++++++++++++++++++ .../TcTableContainer/TcTableRow.spec.tsx | 34 ++++++- .../create-new-announcements.tsx | 2 +- 5 files changed, 217 insertions(+), 14 deletions(-) create mode 100644 src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.spec.tsx create mode 100644 src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx index bfe725b5..8c3146be 100644 --- a/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx @@ -11,6 +11,7 @@ import { SelectChangeEvent, } from '@mui/material'; import TcInput from '../../../shared/TcInput'; +import TcPublicMessagePreviewDialog from './TcPublicMessagePreviewDialog'; const mockPublicChannels = [ { @@ -35,6 +36,9 @@ function TcPublicMessaageContainer() { setMessage(event.target.value); }; + const isPreviewDialogEnabled = + selectedChannels.length > 0 && message.length > 0; + return (
@@ -44,16 +48,9 @@ function TcPublicMessaageContainer() {
-
diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.spec.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.spec.tsx new file mode 100644 index 00000000..1a1e2f98 --- /dev/null +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.spec.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import TcPublicMessagePreviewDialog from './TcPublicMessagePreviewDialog'; + +describe('TcPublicMessagePreviewDialog', () => { + const textMessage = 'This is a test message'; + + it('renders without crashing', () => { + render( + + ); + expect(screen.getByText('Preview')).toBeInTheDocument(); + }); + + it('opens dialog on preview button click', () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + expect(screen.getByText('Preview Public Message')).toBeInTheDocument(); + }); + + it('closes dialog on close icon click', async () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + fireEvent.click(screen.getByTestId('close-icon')); + + await waitFor(() => { + expect( + screen.queryByText('Preview Public Message') + ).not.toBeInTheDocument(); + }); + }); + + it('closes dialog on confirm button click', async () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + fireEvent.click(screen.getByText('Confirm')); + + await waitFor(() => { + expect( + screen.queryByText('Preview Public Message') + ).not.toBeInTheDocument(); + }); + }); + it('displays the correct text message', () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + expect(screen.getByText(textMessage)).toBeInTheDocument(); + }); + + it('preview button is disabled when isPreviewDialogEnabled is false', () => { + render( + + ); + const previewButton = screen.getByRole('button', { name: 'Preview' }); + expect(previewButton).toBeDisabled(); + }); +}); diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx new file mode 100644 index 00000000..a1407175 --- /dev/null +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx @@ -0,0 +1,95 @@ +import React, { useState } from 'react'; +import TcDialog from '../../../shared/TcDialog'; +import TcButton from '../../../shared/TcButton'; +import { AiOutlineClose } from 'react-icons/ai'; +import TcText from '../../../shared/TcText'; + +interface ITcPublicMessagePreviewDialogProps { + textMessage: string; + isPreviewDialogEnabled: boolean; +} + +function TcPublicMessagePreviewDialog({ + textMessage, + isPreviewDialogEnabled, +}: ITcPublicMessagePreviewDialogProps) { + const [isPreviewDialogOpen, setPreviewDialogOpen] = useState(false); + return ( + <> + setPreviewDialogOpen(true)} + /> + +
+ setPreviewDialogOpen(false)} + /> +
+
+ +
+ + {['channel1', 'channel2'].map((channel, index, array) => ( + + {'#'} + + {index < array.length - 1 && ', '} + + ))} +
+ +
+ setPreviewDialogOpen(false)} + sx={{ width: '100%' }} + /> +
+
+ + } + open={isPreviewDialogOpen} + /> + + ); +} + +export default TcPublicMessagePreviewDialog; diff --git a/src/components/shared/TcTableContainer/TcTableRow.spec.tsx b/src/components/shared/TcTableContainer/TcTableRow.spec.tsx index f9eaa7cb..857840cc 100644 --- a/src/components/shared/TcTableContainer/TcTableRow.spec.tsx +++ b/src/components/shared/TcTableContainer/TcTableRow.spec.tsx @@ -1,14 +1,42 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; +import TcTableRow from './TcTableRow'; import TcTableCell from './TcTableCell'; describe('TcTableCell', () => { it('renders correctly with children', () => { - // Render the TcTableCell component with some children render(Sample Content); - - // Check if the rendered content is present const cellContent = screen.getByText('Sample Content'); expect(cellContent).toBeInTheDocument(); }); }); + +describe('TcTableRow', () => { + it('renders correctly with row data', () => { + const rowData = { column1: 'Data1', column2: 'Data2' }; + render(); + expect(screen.getByText('Data1')).toBeInTheDocument(); + expect(screen.getByText('Data2')).toBeInTheDocument(); + }); + + it('applies custom renderers', () => { + const rowData = { column1: 'Data1' }; + const customRenderers = { + column1: (value: any) => {value}, + }; + render(); + const renderedData = screen.getByText('Data1'); + expect(renderedData).toBeInTheDocument(); + expect(renderedData).toHaveProperty('nodeName', 'STRONG'); + }); + + it('applies custom table cell classes', () => { + const rowData = { column1: 'Data1' }; + const customClasses = 'test-class'; + render( + + ); + const cell = screen.getByText('Data1').closest('td'); + expect(cell).toHaveClass(customClasses); + }); +}); diff --git a/src/pages/announcements/create-new-announcements.tsx b/src/pages/announcements/create-new-announcements.tsx index 992dd003..82947d3c 100644 --- a/src/pages/announcements/create-new-announcements.tsx +++ b/src/pages/announcements/create-new-announcements.tsx @@ -14,7 +14,7 @@ function CreateNewAnnouncements() { return ( <> -
+
Date: Mon, 8 Jan 2024 16:01:55 +0300 Subject: [PATCH 18/30] add private preview dialog and edit announcements --- .../TcPrivateMessaageContainer.tsx | 59 +++++--- .../TcPrivateMessagePreviewDialog.spec.tsx | 93 +++++++++++++ .../TcPrivateMessagePreviewDialog.tsx | 126 ++++++++++++++++++ .../TcPublicMessaageContainer.tsx | 12 +- .../TcPublicMessagePreviewDialog.tsx | 21 ++- .../selectPlatform/TcSelectPlatform.tsx | 12 +- .../create-new-announcements.tsx | 2 +- .../announcements/edit-announcements.tsx | 68 ++++++++++ 8 files changed, 364 insertions(+), 29 deletions(-) create mode 100644 src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.spec.tsx create mode 100644 src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.tsx create mode 100644 src/pages/announcements/edit-announcements.tsx diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx index 53141205..96699d4a 100644 --- a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx +++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx @@ -15,6 +15,7 @@ import TcSwitch from '../../../shared/TcSwitch'; import TcIconWithTooltip from '../../../shared/TcIconWithTooltip'; import TcButtonGroup from '../../../shared/TcButtonGroup'; import clsx from 'clsx'; +import TcPrivateMessagePreviewDialog from './TcPrivateMessagePreviewDialog'; const mockPublicChannels = [ { @@ -36,11 +37,17 @@ export enum MessageType { function TcPrivateMessageContainer() { const [privateMessage, setPrivateMessage] = useState(false); const [messageType, setMessageType] = useState(MessageType.Both); - const [selectedChannels, setSelectedChannels] = useState([]); + const [selectedUsernames, setSelectedUsernames] = useState([]); + const [selectedRoles, setSelectedRoles] = useState([]); + const [message, setMessage] = useState(''); - const handleSelectChange = (event: SelectChangeEvent) => { - setSelectedChannels(event.target.value as number[]); + const handleSelectRolesChange = (event: SelectChangeEvent) => { + setSelectedRoles(event.target.value as number[]); + }; + + const handleSelectUsernamesChange = (event: SelectChangeEvent) => { + setSelectedUsernames(event.target.value as number[]); }; const handleChange = (event: React.ChangeEvent) => { @@ -55,6 +62,27 @@ function TcPrivateMessageContainer() { const messageTypesArray = Object.values(MessageType); + const isPreviewDialogEnabled = message.length > 0 && privateMessage == true; + + const getSelectedRolesLabels = () => { + return selectedRoles.map( + (roleId) => + mockPublicChannels.find((channel) => channel.value === roleId)?.label || + '' + ); + }; + + const getSelectedUsernamesLabels = () => { + return selectedUsernames.map( + (usernameId) => + mockPublicChannels.find((channel) => channel.value === usernameId) + ?.label || '' + ); + }; + + const selectedRolesLables = getSelectedRolesLabels(); + const selectedUsernamesLabels = getSelectedUsernamesLabels(); + return (
@@ -62,6 +90,7 @@ function TcPrivateMessageContainer() { + } @@ -104,17 +133,11 @@ function TcPrivateMessageContainer() { /> ))} -
@@ -137,7 +160,7 @@ function TcPrivateMessageContainer() { id="select-standard-label" label="Platform" options={mockPublicChannels} - value={selectedChannels} + value={selectedRoles} renderValue={(selected) => (selected as number[]) .map( @@ -148,7 +171,7 @@ function TcPrivateMessageContainer() { ) .join(', ') } - onChange={(event) => handleSelectChange(event)} + onChange={(event) => handleSelectRolesChange(event)} /> (selected as number[]) .map( @@ -180,7 +203,7 @@ function TcPrivateMessageContainer() { ) .join(', ') } - onChange={(event) => handleSelectChange(event)} + onChange={(event) => handleSelectUsernamesChange(event)} />
diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.spec.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.spec.tsx new file mode 100644 index 00000000..b8a8dec9 --- /dev/null +++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.spec.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import TcPublicMessagePreviewDialog from './TcPrivateMessagePreviewDialog'; + +describe('TcPublicMessagePreviewDialog', () => { + const textMessage = 'This is a test message'; + const roles = ['Admin', 'User']; + const usernames = ['user1', 'user2']; + + it('renders without crashing', () => { + render( + + ); + expect(screen.getByText('Preview')).toBeInTheDocument(); + }); + + it('opens dialog on preview button click', () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + expect(screen.getByText('Preview Private Message')).toBeInTheDocument(); + }); + + it('closes dialog on close icon click', async () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + fireEvent.click(screen.getByTestId('close-icon')); + + await waitFor(() => { + expect( + screen.queryByText('Preview Private Message') + ).not.toBeInTheDocument(); + }); + }); + + it('closes dialog on confirm button click', async () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + fireEvent.click(screen.getByText('Confirm')); + + await waitFor(() => { + expect( + screen.queryByText('Preview Private Message') + ).not.toBeInTheDocument(); + }); + }); + + it('displays the correct text message', () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + expect(screen.getByText(textMessage)).toBeInTheDocument(); + }); + + it('displays roles and usernames when provided', () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + roles.forEach((role) => { + expect(screen.getByText(role)).toBeInTheDocument(); + }); + usernames.forEach((username) => { + expect(screen.getByText(username)).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.tsx new file mode 100644 index 00000000..4e7f865d --- /dev/null +++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.tsx @@ -0,0 +1,126 @@ +import React, { useState } from 'react'; +import TcDialog from '../../../shared/TcDialog'; +import TcButton from '../../../shared/TcButton'; +import { AiOutlineClose } from 'react-icons/ai'; +import TcText from '../../../shared/TcText'; + +interface ITcPublicMessagePreviewDialogProps { + textMessage: string; + selectedRoles?: string[]; + selectedUsernames?: string[]; + isPreviewDialogEnabled: boolean; +} + +function TcPublicMessagePreviewDialog({ + textMessage, + selectedRoles, + selectedUsernames, + isPreviewDialogEnabled, +}: ITcPublicMessagePreviewDialogProps) { + const [isPreviewDialogOpen, setPreviewDialogOpen] = useState(false); + return ( + <> + setPreviewDialogOpen(true)} + /> + +
+ setPreviewDialogOpen(false)} + /> +
+
+ +
+
+ + {selectedRoles && + selectedRoles.map((role, index, array) => ( + + {'#'} + + {index < array.length - 1 && ', '} + + ))} +
+
+ + {selectedUsernames && + selectedUsernames.map((username, index, array) => ( + + {'#'} + + {index < array.length - 1 && ', '} + + ))} +
+
+ +
+ setPreviewDialogOpen(false)} + sx={{ width: '100%' }} + /> +
+
+ + } + open={isPreviewDialogOpen} + /> + + ); +} + +export default TcPublicMessagePreviewDialog; diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx index 8c3146be..0cb87539 100644 --- a/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import TcText from '../../../shared/TcText'; import { MdAnnouncement } from 'react-icons/md'; import TcIconContainer from '../TcIconContainer'; -import TcButton from '../../../shared/TcButton'; import TcSelect from '../../../shared/TcSelect'; import { FormControl, @@ -36,6 +35,16 @@ function TcPublicMessaageContainer() { setMessage(event.target.value); }; + const getSelectedChannelLabels = () => { + return selectedChannels.map( + (channelId) => + mockPublicChannels.find((channel) => channel.value === channelId) + ?.label || '' + ); + }; + + const selectedChannelLabels = getSelectedChannelLabels(); + const isPreviewDialogEnabled = selectedChannels.length > 0 && message.length > 0; @@ -51,6 +60,7 @@ function TcPublicMessaageContainer() {
diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx index a1407175..d4859d9b 100644 --- a/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx @@ -6,11 +6,13 @@ import TcText from '../../../shared/TcText'; interface ITcPublicMessagePreviewDialogProps { textMessage: string; + selectedChannels: string[]; isPreviewDialogEnabled: boolean; } function TcPublicMessagePreviewDialog({ textMessage, + selectedChannels, isPreviewDialogEnabled, }: ITcPublicMessagePreviewDialogProps) { const [isPreviewDialogOpen, setPreviewDialogOpen] = useState(false); @@ -62,13 +64,18 @@ function TcPublicMessagePreviewDialog({ fontWeight={700} className="text-gray-500" /> - {['channel1', 'channel2'].map((channel, index, array) => ( - - {'#'} - - {index < array.length - 1 && ', '} - - ))} + {selectedChannels && + selectedChannels.map((channel, index, array) => ( + + {'#'} + + {index < array.length - 1 && ', '} + + ))}
- +
- + diff --git a/src/pages/announcements/edit-announcements.tsx b/src/pages/announcements/edit-announcements.tsx new file mode 100644 index 00000000..ba934626 --- /dev/null +++ b/src/pages/announcements/edit-announcements.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { defaultLayout } from '../../layouts/defaultLayout'; +import SEO from '../../components/global/SEO'; +import router from 'next/router'; +import TcPrivateMessaageContainer from '../../components/announcements/create/privateMessaageContainer'; +import TcPublicMessaageContainer from '../../components/announcements/create/publicMessageContainer'; +import TcScheduleAnnouncement from '../../components/announcements/create/scheduleAnnouncement'; +import TcSelectPlatform from '../../components/announcements/create/selectPlatform'; +import TcBoxContainer from '../../components/shared/TcBox/TcBoxContainer'; +import TcBreadcrumbs from '../../components/shared/TcBreadcrumbs'; +import TcButton from '../../components/shared/TcButton'; + +function EditAnnouncements() { + return ( + <> + +
+ + +
+ + + + +
+
+ router.push('/announcements')} + variant="outlined" + sx={{ + maxWidth: { + xs: '100%', + sm: '8rem', + }, + }} + /> +
+ +
+
+
+ } + /> +
+ + ); +} + +EditAnnouncements.pageLayout = defaultLayout; + +export default EditAnnouncements; From 8dca83d2f8edcdd8e489aa000fb1880bfe4a50f6 Mon Sep 17 00:00:00 2001 From: zuies Date: Tue, 9 Jan 2024 12:50:12 +0300 Subject: [PATCH 19/30] add confimDialog for create announcements --- ...nfirmSchaduledAnnouncementsDialog.spec.tsx | 80 +++++++++++++ .../TcConfirmSchaduledAnnouncementsDialog.tsx | 105 ++++++++++++++++++ .../create-new-announcements.tsx | 12 +- .../announcements/edit-announcements.tsx | 12 +- src/utils/theme.ts | 8 +- 5 files changed, 194 insertions(+), 23 deletions(-) create mode 100644 src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.spec.tsx create mode 100644 src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx diff --git a/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.spec.tsx b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.spec.tsx new file mode 100644 index 00000000..f2cafeed --- /dev/null +++ b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.spec.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import TcConfirmSchaduledAnnouncementsDialog from './TcConfirmSchaduledAnnouncementsDialog'; + +describe('TcConfirmSchaduledAnnouncementsDialog', () => { + const defaultProps = { + buttonLabel: 'Test Button', + schaduledDate: 'July 12 at 13pm (CET)', + }; + + it('renders without crashing', () => { + render( + + ); + expect(screen.getByText('Test Button')).toBeInTheDocument(); + }); + + it('toggles dialog visibility on button click', async () => { + render( + + ); + const button = screen.getByText('Test Button'); + fireEvent.click(button); + + await waitFor(() => { + expect(screen.getByText('Confirm Schedule')).toBeInTheDocument(); + }); + + const closeButton = screen.getByTestId('close-icon'); + fireEvent.click(closeButton); + + await waitFor(() => { + expect(screen.queryByText('Confirm Schedule')).not.toBeInTheDocument(); + }); + }); + + it('displays the correct dialog content', () => { + render( + + ); + fireEvent.click(screen.getByText('Test Button')); + expect( + screen.getByText('Discord announcements scheduled for:') + ).toBeInTheDocument(); + expect(screen.getByText('Public Message to:')).toBeInTheDocument(); + expect( + screen.getByText('Private Message to these user(s):') + ).toBeInTheDocument(); + expect( + screen.getByText('Private Message to these role(s):') + ).toBeInTheDocument(); + }); + + it('closes the dialog when the close icon is clicked', async () => { + render( + + ); + + fireEvent.click(screen.getByText('Test Button')); + + fireEvent.click(screen.getByTestId('close-icon')); + + await waitFor(() => { + expect(screen.queryByText('Confirm Schedule')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx new file mode 100644 index 00000000..9410bd30 --- /dev/null +++ b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx @@ -0,0 +1,105 @@ +import React, { useState } from 'react'; +import TcButton from '../shared/TcButton'; +import { AiOutlineClose } from 'react-icons/ai'; +import TcDialog from '../shared/TcDialog'; +import TcText from '../shared/TcText'; +import { FaDiscord } from 'react-icons/fa6'; + +interface ITcConfirmSchaduledAnnouncementsDialogProps { + buttonLabel: string; + selectedChannels: string[]; + selectedRoles?: string[]; + selectedUsernames?: string[]; + schaduledDate: string; +} + +function TcConfirmSchaduledAnnouncementsDialog({ + buttonLabel, +}: ITcConfirmSchaduledAnnouncementsDialogProps) { + const [confirmSchadulerDialog, setConfirmSchadulerDialog] = + useState(false); + + return ( + <> + setConfirmSchadulerDialog(true)} + /> + +
+ setConfirmSchadulerDialog(false)} + /> +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ setConfirmSchadulerDialog(false)} + sx={{ width: '100%' }} + /> +
+
+ + } + open={confirmSchadulerDialog} + /> + + ); +} + +export default TcConfirmSchaduledAnnouncementsDialog; diff --git a/src/pages/announcements/create-new-announcements.tsx b/src/pages/announcements/create-new-announcements.tsx index 457b90fc..123da612 100644 --- a/src/pages/announcements/create-new-announcements.tsx +++ b/src/pages/announcements/create-new-announcements.tsx @@ -9,6 +9,7 @@ import TcScheduleAnnouncement from '../../components/announcements/create/schedu import TcSelectPlatform from '../../components/announcements/create/selectPlatform'; import TcBreadcrumbs from '../../components/shared/TcBreadcrumbs'; import router from 'next/router'; +import TcConfirmSchaduledAnnouncementsDialog from '../../components/announcements/TcConfirmSchaduledAnnouncementsDialog'; function CreateNewAnnouncements() { return ( @@ -53,15 +54,8 @@ function CreateNewAnnouncements() { }, }} /> -
diff --git a/src/pages/announcements/edit-announcements.tsx b/src/pages/announcements/edit-announcements.tsx index ba934626..ee711383 100644 --- a/src/pages/announcements/edit-announcements.tsx +++ b/src/pages/announcements/edit-announcements.tsx @@ -9,6 +9,7 @@ import TcSelectPlatform from '../../components/announcements/create/selectPlatfo import TcBoxContainer from '../../components/shared/TcBox/TcBoxContainer'; import TcBreadcrumbs from '../../components/shared/TcBreadcrumbs'; import TcButton from '../../components/shared/TcButton'; +import TcConfirmSchaduledAnnouncementsDialog from '../../components/announcements/TcConfirmSchaduledAnnouncementsDialog'; function EditAnnouncements() { return ( @@ -43,16 +44,7 @@ function EditAnnouncements() { }} />
- +
diff --git a/src/utils/theme.ts b/src/utils/theme.ts index 9e96cf3d..299635df 100644 --- a/src/utils/theme.ts +++ b/src/utils/theme.ts @@ -14,10 +14,10 @@ export const theme = createTheme({ components: { MuiButton: { styleOverrides: { - sizeMedium: { - width: '15rem', - padding: '0.5rem', - }, + // sizeMedium: { + // width: '15rem', + // padding: '0.5rem', + // }, root: { textTransform: 'none', borderRadius: '4px', From a280a1b7e7732f8487ab6d94877eaf9a8148effc Mon Sep 17 00:00:00 2001 From: zuies Date: Mon, 15 Jan 2024 14:31:11 +0300 Subject: [PATCH 20/30] implement basic create method --- .../TcPrivateMessaageContainer.tsx | 19 +- .../TcPublicMessaageContainer.spec.tsx | 32 --- .../TcPublicMessaageContainer.tsx | 112 ---------- .../TcPublicMessageContainer.spec.tsx | 89 ++++++++ .../TcPublicMessageContainer.tsx | 207 ++++++++++++++++++ .../TcPublicMessagePreviewDialog.tsx | 2 +- .../create/publicMessageContainer/index.ts | 4 +- .../TcScheduleAnnouncement.tsx | 19 +- .../shared/TcButtonGroup/TcButtonGroup.tsx | 6 +- src/components/shared/TcSelect/TcSelect.tsx | 35 +-- src/pages/_app.tsx | 21 +- .../create-new-announcements.tsx | 106 ++++++++- src/pages/announcements/index.tsx | 103 +++++---- src/pages/community-settings/index.tsx | 41 ++-- .../community-settings/platform/index.tsx | 23 +- src/pages/index.tsx | 23 +- src/store/slices/announcementsSlice.ts | 35 ++- src/store/types/IAnnouncements.ts | 18 +- src/utils/types.ts | 2 +- 19 files changed, 623 insertions(+), 274 deletions(-) delete mode 100644 src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.spec.tsx delete mode 100644 src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx create mode 100644 src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.spec.tsx create mode 100644 src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.tsx diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx index 96699d4a..a5d4cb08 100644 --- a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx +++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx @@ -90,13 +90,14 @@ function TcPrivateMessageContainer() { + } label={
- + {privateMessage && (
+
+ + +
+
+ + +
{ - test('renders the component without crashing', () => { - render(); - expect(screen.getByText('Public Message')).toBeInTheDocument(); - }); - - test('initial state is set correctly', () => { - render(); - expect(screen.getByPlaceholderText('Write your message here')).toHaveValue( - '' - ); - }); - - test('allows the user to enter a message', () => { - render(); - const messageInput = screen.getByPlaceholderText( - 'Write your message here' - ) as HTMLInputElement; - fireEvent.change(messageInput, { target: { value: 'Test Message' } }); - expect(messageInput.value).toBe('Test Message'); - }); - - test('select channels dropdown is rendered', () => { - render(); - expect(screen.getByLabelText('Select Channels')).toBeInTheDocument(); - }); -}); diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx deleted file mode 100644 index 0cb87539..00000000 --- a/src/components/announcements/create/publicMessageContainer/TcPublicMessaageContainer.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useState } from 'react'; -import TcText from '../../../shared/TcText'; -import { MdAnnouncement } from 'react-icons/md'; -import TcIconContainer from '../TcIconContainer'; -import TcSelect from '../../../shared/TcSelect'; -import { - FormControl, - FormHelperText, - InputLabel, - SelectChangeEvent, -} from '@mui/material'; -import TcInput from '../../../shared/TcInput'; -import TcPublicMessagePreviewDialog from './TcPublicMessagePreviewDialog'; - -const mockPublicChannels = [ - { - label: 'test', - value: 1, - }, - { - label: 'test2', - value: 2, - }, -]; - -function TcPublicMessaageContainer() { - const [selectedChannels, setSelectedChannels] = useState([]); - const [message, setMessage] = useState(''); - - const handleSelectChange = (event: SelectChangeEvent) => { - setSelectedChannels(event.target.value as number[]); - }; - - const handleChange = (event: React.ChangeEvent) => { - setMessage(event.target.value); - }; - - const getSelectedChannelLabels = () => { - return selectedChannels.map( - (channelId) => - mockPublicChannels.find((channel) => channel.value === channelId) - ?.label || '' - ); - }; - - const selectedChannelLabels = getSelectedChannelLabels(); - - const isPreviewDialogEnabled = - selectedChannels.length > 0 && message.length > 0; - - return ( -
-
-
- - - - -
- -
-
- - Select Channels - - (selected as number[]) - .map( - (value) => - mockPublicChannels.find( - (channel) => channel.value === value - )?.label - ) - .join(', ') - } - onChange={(event) => handleSelectChange(event)} - /> - - The announcement will be sent by the a bot which will have access to - send the following message within the selected channels - - - - - -
-
- ); -} - -export default TcPublicMessaageContainer; diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.spec.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.spec.tsx new file mode 100644 index 00000000..56b1abfb --- /dev/null +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.spec.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import TcPublicMessageContainer from './TcPublicMessageContainer'; +import { ChannelContext } from '../../../../context/ChannelContext'; + +const mockChannels = [ + { channelId: '1131241242', title: 'Channel 1', subChannels: [] }, + { channelId: '1242512553', title: 'Channel 2', subChannels: [] }, +]; + +const mockSelectedSubChannels = { + channel1: { '1131241242': true }, + channel2: { '1242512553': true }, +}; + +const mockChannelContext = { + channels: mockChannels, + selectedSubChannels: mockSelectedSubChannels, + loading: false, + refreshData: jest.fn(), + handleSubChannelChange: jest.fn(), + handleSelectAll: jest.fn(), + updateSelectedSubChannels: jest.fn(), +}; + +describe('TcPublicMessageContainer Tests', () => { + // Helper function to render the component with the necessary context + const renderComponent = (handlePublicAnnouncements = jest.fn()) => + render( + + + + ); + + test('renders the component without crashing', () => { + renderComponent(); + expect(screen.getByText('Public Message')).toBeInTheDocument(); + }); + + test('initial state is set correctly', () => { + renderComponent(); + expect(screen.getByPlaceholderText('Write your message here')).toHaveValue( + '' + ); + }); + + test('allows the user to enter a message', () => { + renderComponent(); + const messageInput = screen.getByPlaceholderText( + 'Write your message here' + ) as HTMLInputElement; + fireEvent.change(messageInput, { target: { value: 'Test Message' } }); + expect(messageInput.value).toBe('Test Message'); + }); + + test('select channels dropdown is rendered', () => { + renderComponent(); + expect(screen.getByLabelText('Select Channels')).toBeInTheDocument(); + }); + + test('handlePublicAnnouncements is called with correct data', () => { + const handlePublicAnnouncementsMock = jest.fn(); + renderComponent(handlePublicAnnouncementsMock); + + // Assuming there is a way to select channels in your UI, simulate that + // For example, if there's a button to confirm channel selection: + // fireEvent.click(screen.getByText('Confirm Channels')); + + // Simulate entering a message + const messageInput = screen.getByPlaceholderText( + 'Write your message here' + ) as HTMLInputElement; + fireEvent.change(messageInput, { target: { value: 'Test Message' } }); + + // Assuming the function is called on some action, like a form submission or button click + // fireEvent.click(screen.getByText('Submit')); + + // Check if handlePublicAnnouncementsMock was called correctly + // Expect the mock to have been called with expected message and channels data + // This will depend on how your component calls the handlePublicAnnouncements function + expect(handlePublicAnnouncementsMock).toHaveBeenCalledWith({ + message: 'Test Message', + selectedChannels: expect.anything(), // Replace with specific expectation + }); + }); +}); diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.tsx new file mode 100644 index 00000000..34116b48 --- /dev/null +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.tsx @@ -0,0 +1,207 @@ +import React, { useContext, useEffect, useState } from 'react'; +import TcText from '../../../shared/TcText'; +import { MdAnnouncement, MdExpandMore } from 'react-icons/md'; +import TcIconContainer from '../TcIconContainer'; +import TcSelect from '../../../shared/TcSelect'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + FormControl, + FormHelperText, + InputLabel, +} from '@mui/material'; +import TcInput from '../../../shared/TcInput'; +import TcPublicMessagePreviewDialog from './TcPublicMessagePreviewDialog'; +import { ChannelContext } from '../../../../context/ChannelContext'; +import TcPlatformChannelList from '../../../communitySettings/platform/TcPlatformChannelList'; +import { IGuildChannels } from '../../../../utils/types'; + +export interface FlattenedChannel { + id: string; + label: string; +} + +export interface ITcPublicMessageContainerProps { + handlePublicAnnouncements: ({ + message, + selectedChannels, + }: { + message: string; + selectedChannels: FlattenedChannel[]; + }) => void; +} + +function TcPublicMessageContainer({ + handlePublicAnnouncements, +}: ITcPublicMessageContainerProps) { + const channelContext = useContext(ChannelContext); + + const { channels, selectedSubChannels } = channelContext; + + const flattenChannels = (channels: IGuildChannels[]): FlattenedChannel[] => { + let flattened: FlattenedChannel[] = []; + + channels.forEach((channel) => { + if (channel.subChannels) { + channel.subChannels.forEach((subChannel) => { + if (selectedSubChannels[channel.channelId]?.[subChannel.channelId]) { + flattened.push({ + id: subChannel.channelId, + label: subChannel.name, + }); + } + }); + } + }); + + return flattened; + }; + + const [selectedChannels, setSelectedChannels] = useState( + [] + ); + + useEffect(() => { + setSelectedChannels(flattenChannels(channels)); + }, [channels, selectedSubChannels]); + + const [message, setMessage] = useState(''); + + const handleChange = (event: React.ChangeEvent) => { + setMessage(event.target.value); + }; + + const isPreviewDialogEnabled = + selectedChannels.length > 0 && message.length > 0; + + useEffect(() => { + handlePublicAnnouncements({ message, selectedChannels }); + }, [message, selectedChannels]); + + return ( +
+
+
+ + + + +
+ channel.label)} + /> +
+
+
+ + +
+ + Select Channels + + (selected as FlattenedChannel[]) + .map((channel) => `#${channel.label}`) + .join(', ') + } + > +
+
+ +
+
+ + + } + > + + + +
+ +
    +
  1. + Navigate to the channel you want to import on{' '} + + Discord + +
  2. +
  3. + Go to the settings for that specific channel (select + the wheel on the right of the channel name) +
  4. +
  5. + Select Permissions (left sidebar), and then in + the middle of the screen check{' '} + Advanced permissions +
  6. +
  7. + With the TogetherCrew Bot selected, under + Advanced Permissions, make sure that [View channel] + and [Write Access] are marked as [✓] +
  8. +
  9. + Select the plus sign to the right of Roles/Members and + under members select TogetherCrew bot +
  10. +
  11. + Click on the Refresh List button on this window + and select the new channels +
  12. +
+
+
+
+
+
+
+
+ + + + +
+
+ ); +} + +export default TcPublicMessageContainer; diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx index d4859d9b..15274c87 100644 --- a/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx @@ -57,7 +57,7 @@ function TcPublicMessagePreviewDialog({ variant="h5" className="pb-4" /> -
+
void; +} + +function TcScheduleAnnouncement({ + handleSchaduledDate, +}: ITcScheduleAnnouncementProps) { const [anchorEl, setAnchorEl] = useState(null); const [activeTab, setActiveTab] = useState(0); const [selectedDate, setSelectedDate] = useState(null); @@ -48,6 +54,15 @@ function TcScheduleAnnouncement() { } }; + useEffect(() => { + if (!selectedTime) return; + + const formattedTime = selectedTime.toISOString(); + console.log({ formattedTime }); + + handleSchaduledDate({ selectedTime: formattedTime }); + }, [selectedTime]); + return (
diff --git a/src/components/shared/TcButtonGroup/TcButtonGroup.tsx b/src/components/shared/TcButtonGroup/TcButtonGroup.tsx index a6433c31..3ea16c9f 100644 --- a/src/components/shared/TcButtonGroup/TcButtonGroup.tsx +++ b/src/components/shared/TcButtonGroup/TcButtonGroup.tsx @@ -1,11 +1,11 @@ import { ButtonGroup, ButtonGroupProps } from '@mui/material'; -import React, { ReactElement, ReactNode } from 'react'; +import React, { ReactNode } from 'react'; -interface TcButtonGroup extends ButtonGroupProps { +interface ITcButtonGroup extends ButtonGroupProps { children: ReactNode; } -function TcButtonGroup({ children, ...props }: TcButtonGroup) { +function TcButtonGroup({ children, ...props }: ITcButtonGroup) { return {children}; } diff --git a/src/components/shared/TcSelect/TcSelect.tsx b/src/components/shared/TcSelect/TcSelect.tsx index abfa847f..124904ca 100644 --- a/src/components/shared/TcSelect/TcSelect.tsx +++ b/src/components/shared/TcSelect/TcSelect.tsx @@ -14,12 +14,13 @@ interface ITcSelectProps extends SelectProps { * - label (string): The display label for the option * - icon (ReactElement): Optional icon to display alongside the label */ - options: Array<{ + options?: Array<{ value: string | number; label: string; icon?: ReactElement; disabled?: boolean; }>; + children?: React.ReactNode; } /** @@ -30,21 +31,27 @@ interface ITcSelectProps extends SelectProps { * @returns {ReactElement} The TcSelect component */ -function TcSelect({ options, ...props }: ITcSelectProps): ReactElement { +function TcSelect({ + options, + children, + ...props +}: ITcSelectProps): ReactElement { return ( ); } diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 39959383..ea9dc52d 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -24,6 +24,7 @@ import Script from 'next/script'; import { usePageViewTracking } from '../helpers/amplitudeHelper'; import SafaryClubScript from '../components/global/SafaryClubScript'; import { TokenProvider } from '../context/TokenContext'; +import { ChannelProvider } from '../context/ChannelContext'; export default function App({ Component, pageProps }: ComponentWithPageLayout) { usePageViewTracking(); @@ -58,15 +59,17 @@ export default function App({ Component, pageProps }: ComponentWithPageLayout) { - {Component.pageLayout ? ( - - - - - - ) : ( - - )} + + {Component.pageLayout ? ( + + + + + + ) : ( + + )} + diff --git a/src/pages/announcements/create-new-announcements.tsx b/src/pages/announcements/create-new-announcements.tsx index 123da612..9298382b 100644 --- a/src/pages/announcements/create-new-announcements.tsx +++ b/src/pages/announcements/create-new-announcements.tsx @@ -1,8 +1,8 @@ -import React from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { defaultLayout } from '../../layouts/defaultLayout'; import SEO from '../../components/global/SEO'; import TcBoxContainer from '../../components/shared/TcBox/TcBoxContainer'; -import TcPublicMessaageContainer from '../../components/announcements/create/publicMessageContainer'; +import TcPublicMessageContainer from '../../components/announcements/create/publicMessageContainer/TcPublicMessageContainer'; import TcPrivateMessaageContainer from '../../components/announcements/create/privateMessaageContainer'; import TcButton from '../../components/shared/TcButton'; import TcScheduleAnnouncement from '../../components/announcements/create/scheduleAnnouncement/'; @@ -10,8 +10,81 @@ import TcSelectPlatform from '../../components/announcements/create/selectPlatfo import TcBreadcrumbs from '../../components/shared/TcBreadcrumbs'; import router from 'next/router'; import TcConfirmSchaduledAnnouncementsDialog from '../../components/announcements/TcConfirmSchaduledAnnouncementsDialog'; +import useAppStore from '../../store/useStore'; +import { useToken } from '../../context/TokenContext'; +import { ChannelContext } from '../../context/ChannelContext'; + +export type CreateAnnouncementsPayloadDataOptions = + | { channelIds: string[]; userIds?: string[]; roleIds?: string[] } + | { channelIds?: string[]; userIds: string[]; roleIds?: string[] } + | { channelIds?: string[]; userIds?: string[]; roleIds: string[] }; + +export interface CreateAnnouncementsPayloadData { + platformId: string; + template: string; + options: CreateAnnouncementsPayloadDataOptions; +} +export interface CreateAnnouncementsPayload { + title: string; + communityId: string; + scheduledAt: string; + draft: boolean; + data: CreateAnnouncementsPayloadData[]; +} function CreateNewAnnouncements() { + const { createNewAnnouncements, retrievePlatformById } = useAppStore(); + + const { community } = useToken(); + + const channelContext = useContext(ChannelContext); + + const { refreshData } = channelContext; + + const platformId = community?.platforms.find( + (platform) => platform.disconnectedAt === null + )?.id; + + const [publicAnnouncements, setPublicAnnouncements] = + useState(); + const [scheduledAt, setScheduledAt] = useState(); + + const fetchPlatformChannels = async () => { + try { + if (platformId) { + const data = await retrievePlatformById(platformId); + const { metadata } = data; + if (metadata) { + const { selectedChannels } = metadata; + await refreshData(platformId, 'channel', selectedChannels, true); + } else { + await refreshData(platformId); + } + } + } catch (error) { + } finally { + } + }; + + useEffect(() => { + if (!platformId) { + return; + } + + fetchPlatformChannels(); + }, [platformId]); + + const handleCreateAnnouncements = (isDrafted: boolean) => { + if (!community) return; + const announcementsPayload = { + communityId: community.id, + draft: isDrafted, + scheduledAt: scheduledAt, + data: [publicAnnouncements], + }; + + createNewAnnouncements(announcementsPayload); + }; return ( <> @@ -24,12 +97,32 @@ function CreateNewAnnouncements() { /> +
- + { + if (!platformId) return; + setPublicAnnouncements({ + platformId: platformId, + template: message, + options: { + channelIds: selectedChannels.map( + (channel) => channel.id + ), + }, + }); + }} + /> - + { + setScheduledAt(selectedTime); + }} + />
handleCreateAnnouncements(true)} />
diff --git a/src/pages/announcements/index.tsx b/src/pages/announcements/index.tsx index 215dd2cb..c56675d7 100644 --- a/src/pages/announcements/index.tsx +++ b/src/pages/announcements/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { defaultLayout } from '../../layouts/defaultLayout'; import TcBoxContainer from '../../components/shared/TcBox/TcBoxContainer'; import SEO from '../../components/global/SEO'; @@ -12,33 +12,18 @@ import TcTimeZone from '../../components/announcements/TcTimeZone'; import TcDateTimePopover from '../../components/announcements/create/scheduleAnnouncement/TcDateTimePopover'; import moment from 'moment'; import { MdCalendarMonth } from 'react-icons/md'; +import useAppStore from '../../store/useStore'; +import { StorageService } from '../../services/StorageService'; +import { FetchedData, IDiscordModifiedCommunity } from '../../utils/interfaces'; const headers = ['Announcement', 'Channel', 'Handle', 'Role', 'Date']; -const bodyRowItems: any[] = [ - // { - // Announcement: 'Lorem Ipsum Announcement', - // Channel: 'General', - // Handle: 'JohnDoe', - // Role: 'Admin', - // Date: '2023-03-15', - // }, - // { - // Announcement: 'New Feature Release', - // Channel: 'Product Updates', - // Handle: 'JaneSmith', - // Role: 'User', - // Date: '2023-03-20', - // }, - // { - // Announcement: 'Upcoming Event', - // Channel: 'Events', - // Handle: 'EventHost', - // Role: 'Moderator', - // Date: '2023-04-05', - // }, -]; function Index() { + const { retrieveAnnouncements } = useAppStore(); + + const communityId = + StorageService.readLocalStorage('community')?.id; + const [anchorEl, setAnchorEl] = useState(null); const [activeTab, setActiveTab] = useState(0); const [selectedDate, setSelectedDate] = useState(null); @@ -47,6 +32,17 @@ function Index() { moment().format('D MMMM YYYY @ h A') ); + const [loading, setLoading] = useState(false); + const [fetchedAnnouncements, setFetchedAnnouncements] = useState( + { + limit: 10, + page: 1, + results: [], + totalPages: 0, + totalResults: 0, + } + ); + const handleOpen = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -80,6 +76,41 @@ function Index() { } }; + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + + const data = await retrieveAnnouncements({ + page: 1, + limit: 10, + community: communityId, + }); + + setFetchedAnnouncements(data); + setLoading(false); + } catch (error) { + console.error('An error occurred while fetching platforms:', error); + setLoading(false); + } + }; + + fetchData(); + }, []); + + const formatAnnouncementsForTable = () => { + console.log(fetchedAnnouncements.results); + + return fetchedAnnouncements.results.map( + (announcement) => console.log(announcement.data.options) + + // { + // Announcement: announcement.title, + // Date: moment(announcement.scheduledAt).format('YYYY-MM-DD'), + // } + ); + }; + return ( <> @@ -123,10 +154,10 @@ function Index() { />
- {bodyRowItems.length > 0 ? ( + {fetchedAnnouncements.results.length > 0 ? ( ) : (
@@ -145,18 +176,14 @@ function Index() {
- {bodyRowItems.length > 0 ? ( - - ) : ( - '' - )} +
} diff --git a/src/pages/community-settings/index.tsx b/src/pages/community-settings/index.tsx index 4f7cbc10..a7835f67 100644 --- a/src/pages/community-settings/index.tsx +++ b/src/pages/community-settings/index.tsx @@ -8,7 +8,6 @@ import TcIntegrationDialog from '../../components/pages/communitySettings/TcInte import { useRouter } from 'next/router'; import TcSwitchCommunity from '../../components/communitySettings/switchCommunity/TcSwitchCommunity'; import SimpleBackdrop from '../../components/global/LoadingBackdrop'; -import { ChannelProvider } from '../../context/ChannelContext'; function index() { const router = useRouter(); @@ -48,29 +47,27 @@ function index() { return ( <> - - -
- - -
- - -
+ +
+ + +
+ +
- } - /> -
- + } /> - +
+ ); } diff --git a/src/pages/community-settings/platform/index.tsx b/src/pages/community-settings/platform/index.tsx index 6c341877..f1f6e5a3 100644 --- a/src/pages/community-settings/platform/index.tsx +++ b/src/pages/community-settings/platform/index.tsx @@ -2,24 +2,21 @@ import TcPlatform from '../../../components/communitySettings/platform'; import SEO from '../../../components/global/SEO'; import TcBreadcrumbs from '../../../components/shared/TcBreadcrumbs'; -import { ChannelProvider } from '../../../context/ChannelContext'; import { defaultLayout } from '../../../layouts/defaultLayout'; function Index() { return ( <> - - -
- - -
-
+ +
+ + +
); } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 3aae153d..a5613a93 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -7,7 +7,6 @@ import React from 'react'; import ActiveMemberComposition from '../components/pages/pageIndex/ActiveMemberComposition'; import HeatmapChart from '../components/pages/pageIndex/HeatmapChart'; import MemberInteractionGraph from '../components/pages/pageIndex/MemberInteractionGraph'; -import { ChannelProvider } from '../context/ChannelContext'; import { useToken } from '../context/TokenContext'; function Dashboard(): JSX.Element { @@ -25,20 +24,18 @@ function Dashboard(): JSX.Element { return ( <> - -
-
-

- Community Insights -

-
- - - -
+
+
+

+ Community Insights +

+
+ + +
- +
); } diff --git a/src/store/slices/announcementsSlice.ts b/src/store/slices/announcementsSlice.ts index a8adaa5c..96595163 100644 --- a/src/store/slices/announcementsSlice.ts +++ b/src/store/slices/announcementsSlice.ts @@ -1,11 +1,31 @@ import { StateCreator } from 'zustand'; import { axiosInstance } from '../../axiosInstance'; -import IAnnouncements from '../types/IAnnouncements'; +import IAnnouncements, { + IRetrieveAnnouncementsProps, +} from '../types/IAnnouncements'; +import { CreateAnnouncementsPayload } from '../../pages/announcements/create-new-announcements'; const createAnnouncementsSlice: StateCreator = (set, get) => ({ - retrieveAnnouncements: async () => { + retrieveAnnouncements: async ({ + page, + limit, + sortBy, + name, + community, + }: IRetrieveAnnouncementsProps) => { try { - const { data } = await axiosInstance.get(`/announcements/`); + const params = { + page, + limit, + sortBy, + ...(name ? { name } : {}), + }; + + const { data } = await axiosInstance.get( + `/announcements/?communityId=${community}`, + { params } + ); + return data; } catch (error) { console.error('Failed to retrieve announcements:', error); @@ -19,9 +39,14 @@ const createAnnouncementsSlice: StateCreator = (set, get) => ({ console.error('Failed to retrieve announcement:', error); } }, - createNewAnnouncements: async () => { + createNewAnnouncements: async ( + announcementPayload: CreateAnnouncementsPayload + ) => { try { - const { data } = await axiosInstance.post(`/announcements/`); + const { data } = await axiosInstance.post( + `/announcements/`, + announcementPayload + ); return data; } catch (error) { console.error('Failed to create announcements:', error); diff --git a/src/store/types/IAnnouncements.ts b/src/store/types/IAnnouncements.ts index 7691c89c..916030ae 100644 --- a/src/store/types/IAnnouncements.ts +++ b/src/store/types/IAnnouncements.ts @@ -1 +1,17 @@ -export default interface IAnnouncements {} +export interface IRetrieveAnnouncementsProps { + page: number; + limit: number; + sortBy?: string; + name?: string; + community: string; +} + +export default interface IAnnouncements { + retrieveAnnouncements: ({ + page, + limit, + sortBy, + name, + community, + }: IRetrieveAnnouncementsProps) => void; +} diff --git a/src/utils/types.ts b/src/utils/types.ts index c5c59ae7..1b606fea 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -41,7 +41,7 @@ export type ISubChannels = { readonly channelId: string; readonly name: string; readonly canReadMessageHistoryAndViewChannel: boolean; - readonly parent_id: string; + readonly parent_id?: string; }; export type IChannel = { From b6cc0bbdf54e6d3d21ba7e453f68fcc3a72308ff Mon Sep 17 00:00:00 2001 From: zuies Date: Wed, 17 Jan 2024 05:12:41 +0300 Subject: [PATCH 21/30] complete create announcements integration --- src/axiosInstance.ts | 16 ++ .../TcConfirmSchaduledAnnouncementsDialog.tsx | 127 +++++++++---- .../TcPrivateMessaageContainer.tsx | 157 +++++++-------- .../TcPrivateMessagePreviewDialog.tsx | 10 +- .../TcRolesAutoComplete.tsx | 179 ++++++++++++++++++ .../TcUsersAutoComplete.tsx | 169 +++++++++++++++++ .../TcPublicMessageContainer.tsx | 1 - .../TcScheduleAnnouncement.tsx | 16 +- src/components/shared/TcAutocomplete.tsx | 35 ++++ src/context/ChannelContext.tsx | 18 +- .../create-new-announcements.tsx | 100 ++++++++-- src/store/types/IPlatform.ts | 2 +- src/utils/interfaces.ts | 9 + 13 files changed, 691 insertions(+), 148 deletions(-) create mode 100644 src/components/announcements/create/privateMessaageContainer/TcRolesAutoComplete.tsx create mode 100644 src/components/announcements/create/privateMessaageContainer/TcUsersAutoComplete.tsx create mode 100644 src/components/shared/TcAutocomplete.tsx diff --git a/src/axiosInstance.ts b/src/axiosInstance.ts index 4b118797..d3a0b4cc 100644 --- a/src/axiosInstance.ts +++ b/src/axiosInstance.ts @@ -169,6 +169,22 @@ axiosInstance.interceptors.response.use( }); window.location.href = '/'; + Sentry.captureException( + new Error( + `API responded with status code ${error.response.status}: ${error.response.data.message}` + ) + ); + break; + case 500: + toast.error(`${error.response.data.message}`, { + position: 'bottom-left', + autoClose: 5000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: 0, + }); Sentry.captureException( new Error( `API responded with status code ${error.response.status}: ${error.response.data.message}` diff --git a/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx index 9410bd30..b411c4f2 100644 --- a/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx +++ b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx @@ -4,17 +4,40 @@ import { AiOutlineClose } from 'react-icons/ai'; import TcDialog from '../shared/TcDialog'; import TcText from '../shared/TcText'; import { FaDiscord } from 'react-icons/fa6'; +import moment from 'moment'; +import { IRoles, IUser } from '../../utils/interfaces'; interface ITcConfirmSchaduledAnnouncementsDialogProps { buttonLabel: string; - selectedChannels: string[]; - selectedRoles?: string[]; - selectedUsernames?: string[]; + selectedChannels: { id: string; label: string }[]; + selectedRoles?: IRoles[]; + selectedUsernames?: IUser[]; schaduledDate: string; + isDisabled: boolean; + handleCreateAnnouncements: (isDrafted: boolean) => void; } +const formatDateToLocalTimezone = (scheduledDate: string) => { + if (!scheduledDate) { + console.error('Scheduled date is undefined or null'); + return 'Invalid Date'; + } + + const formattedDate = moment(scheduledDate).format('MMMM D [at] hA'); + + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + return `${formattedDate} (${timezone})`; +}; + function TcConfirmSchaduledAnnouncementsDialog({ buttonLabel, + schaduledDate, + selectedRoles, + selectedUsernames, + selectedChannels, + isDisabled = true, + handleCreateAnnouncements, }: ITcConfirmSchaduledAnnouncementsDialogProps) { const [confirmSchadulerDialog, setConfirmSchadulerDialog] = useState(false); @@ -24,6 +47,7 @@ function TcConfirmSchaduledAnnouncementsDialog({ setConfirmSchadulerDialog(true)} /> setConfirmSchadulerDialog(false)} />
-
- +
+
@@ -57,39 +85,72 @@ function TcConfirmSchaduledAnnouncementsDialog({ className="text-left" />
-
- - -
-
- - -
-
- - +
+
+ + +
+ {selectedChannels + .map((channel) => `#${channel.label}`) + .join(', ')}
+ {selectedUsernames && selectedUsernames.length > 0 ? ( +
+
+ + {' '} +
+ {selectedChannels + .map((channel) => `#${channel.label}`) + .join(', ')} +
+ ) : ( + '' + )} + {selectedRoles && selectedRoles.length > 0 ? ( +
+
+ + +
+ {selectedRoles.map((role) => `#${role.name}`).join(', ')} +
+ ) : ( + '' + )}
setConfirmSchadulerDialog(false)} + onClick={() => { + setConfirmSchadulerDialog(false); + handleCreateAnnouncements(false); + }} sx={{ width: '100%' }} />
diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx index a5d4cb08..78058436 100644 --- a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx +++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx @@ -1,32 +1,18 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import TcText from '../../../shared/TcText'; import { MdOutlineAnnouncement } from 'react-icons/md'; import TcIconContainer from '../TcIconContainer'; import TcButton from '../../../shared/TcButton'; -import TcSelect from '../../../shared/TcSelect'; -import { - FormControl, - FormControlLabel, - InputLabel, - SelectChangeEvent, -} from '@mui/material'; +import { FormControl, FormControlLabel } from '@mui/material'; import TcInput from '../../../shared/TcInput'; import TcSwitch from '../../../shared/TcSwitch'; import TcIconWithTooltip from '../../../shared/TcIconWithTooltip'; import TcButtonGroup from '../../../shared/TcButtonGroup'; import clsx from 'clsx'; import TcPrivateMessagePreviewDialog from './TcPrivateMessagePreviewDialog'; - -const mockPublicChannels = [ - { - label: 'test', - value: 1, - }, - { - label: 'test2', - value: 2, - }, -]; +import TcRolesAutoComplete from './TcRolesAutoComplete'; +import TcUsersAutoComplete from './TcUsersAutoComplete'; +import { IRoles, IUser } from '../../../../utils/interfaces'; export enum MessageType { Both = 'Both', @@ -34,22 +20,28 @@ export enum MessageType { UserOnly = 'User Only', } -function TcPrivateMessageContainer() { +export interface ITcPrivateMessageContainerProps { + handlePrivateAnnouncements: ({ + message, + selectedRoles, + selectedUsers, + }: { + message: string; + selectedRoles?: IRoles[]; + selectedUsers?: IUser[]; + }) => void; +} + +function TcPrivateMessageContainer({ + handlePrivateAnnouncements, +}: ITcPrivateMessageContainerProps) { const [privateMessage, setPrivateMessage] = useState(false); const [messageType, setMessageType] = useState(MessageType.Both); - const [selectedUsernames, setSelectedUsernames] = useState([]); - const [selectedRoles, setSelectedRoles] = useState([]); + const [selectedUsers, setSelectedUsers] = useState([]); + const [selectedRoles, setSelectedRoles] = useState([]); const [message, setMessage] = useState(''); - const handleSelectRolesChange = (event: SelectChangeEvent) => { - setSelectedRoles(event.target.value as number[]); - }; - - const handleSelectUsernamesChange = (event: SelectChangeEvent) => { - setSelectedUsernames(event.target.value as number[]); - }; - const handleChange = (event: React.ChangeEvent) => { setMessage(event.target.value); }; @@ -65,23 +57,49 @@ function TcPrivateMessageContainer() { const isPreviewDialogEnabled = message.length > 0 && privateMessage == true; const getSelectedRolesLabels = () => { - return selectedRoles.map( - (roleId) => - mockPublicChannels.find((channel) => channel.value === roleId)?.label || - '' - ); + return selectedRoles.map((role) => role.name || ''); }; - const getSelectedUsernamesLabels = () => { - return selectedUsernames.map( - (usernameId) => - mockPublicChannels.find((channel) => channel.value === usernameId) - ?.label || '' - ); + const selectedRolesLables = getSelectedRolesLabels(); + + const getSelectedUsersLabels = () => { + return selectedUsers.map((user) => user.ngu || ''); }; - const selectedRolesLables = getSelectedRolesLabels(); - const selectedUsernamesLabels = getSelectedUsernamesLabels(); + const selectedUsersLables = getSelectedUsersLabels(); + + useEffect(() => { + const prepareAndSendData = () => { + switch (messageType) { + case MessageType.Both: + handlePrivateAnnouncements({ message, selectedRoles, selectedUsers }); + break; + + case MessageType.RoleOnly: + handlePrivateAnnouncements({ message, selectedRoles }); + break; + + case MessageType.UserOnly: + handlePrivateAnnouncements({ message, selectedUsers }); + break; + + default: + handlePrivateAnnouncements({ message, selectedRoles, selectedUsers }); + break; + } + }; + + if (message && privateMessage) { + prepareAndSendData(); + } + }, [ + message, + selectedRoles, + selectedUsers, + messageType, + privateMessage, + handlePrivateAnnouncements, + ]); return (
@@ -91,7 +109,6 @@ function TcPrivateMessageContainer() { - } @@ -136,8 +153,8 @@ function TcPrivateMessageContainer() {
@@ -162,25 +179,12 @@ function TcPrivateMessageContainer() { messageType !== MessageType.RoleOnly } > - Select Role(s) - - (selected as number[]) - .map( - (value) => - mockPublicChannels.find( - (channel) => channel.value === value - )?.label - ) - .join(', ') + handleSelectRolesChange(event)} + handleSelectedUsers={setSelectedRoles} /> - - Select Username(s) - - - (selected as number[]) - .map( - (value) => - mockPublicChannels.find( - (channel) => channel.value === value - )?.label - ) - .join(', ') + handleSelectUsernamesChange(event)} + handleSelectedUsers={setSelectedUsers} />
diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.tsx index 4e7f865d..3c8683e9 100644 --- a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.tsx +++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.tsx @@ -60,7 +60,7 @@ function TcPublicMessagePreviewDialog({ className="pb-4" />
-
+
{selectedRoles && selectedRoles.map((role, index, array) => ( - - {'#'} + + {'@'} ))}
-
+
void; +} + +function TcRolesAutoComplete({ + isDisabled, + handleSelectedUsers, +}: ITcRolesAutoCompleteProps) { + const { community } = useToken(); + + const platformId = community?.platforms.find( + (platform) => platform.disconnectedAt === null + )?.id; + + const { retrievePlatformProperties } = useAppStore(); + const [selectedRoles, setSelectedRoles] = useState([]); + + const [fetchedRoles, setFetchedRoles] = useState({ + limit: 8, + page: 1, + results: [], + totalPages: 0, + totalResults: 0, + }); + const [filteredRolesByName, setFilteredRolesByName] = useState(''); + + const fetchDiscordRoles = async ( + platformId: string, + page?: number, + limit?: number, + name?: string + ) => { + try { + const fetchedRoles = await retrievePlatformProperties({ + platformId, + name: name, + property: 'role', + page: page, + limit: limit, + }); + + if (name) { + setFilteredRolesByName(name); + setFetchedRoles(fetchedRoles); + } else { + setFetchedRoles((prevData: { results: any }) => { + const updatedResults = [ + ...prevData.results, + ...fetchedRoles.results, + ].filter( + (role, index, self) => + index === self.findIndex((r) => r.id === role.id) + ); + + return { + ...prevData, + ...fetchedRoles, + results: updatedResults, + }; + }); + } + } catch (error) {} + }; + + useEffect(() => { + if (!platformId) return; + fetchDiscordRoles(platformId, fetchedRoles.page, fetchedRoles.limit); + }, []); + + const debouncedFetchDiscordRoles = debounce(fetchDiscordRoles, 700); + + const handleSearchChange = (event: React.SyntheticEvent) => { + const target = event.target as HTMLInputElement; + const inputValue = target.value; + + if (!platformId) return; + + if (inputValue === '') { + setFilteredRolesByName(''); + setFetchedRoles({ + limit: 8, + page: 1, + results: [], + totalPages: 0, + totalResults: 0, + }); + + debouncedFetchDiscordRoles(platformId, 1, 8); + } else { + debouncedFetchDiscordRoles(platformId, 1, 8, inputValue); + } + }; + + const handleScroll = (event: React.UIEvent) => { + const listboxNode = event.currentTarget; + if ( + listboxNode.scrollTop + listboxNode.clientHeight === + listboxNode.scrollHeight + ) { + const nextPage = + Math.ceil(fetchedRoles.results.length / fetchedRoles.limit) + 1; + if (fetchedRoles.totalPages >= nextPage) { + if (!platformId) return; + fetchDiscordRoles(platformId, nextPage, fetchedRoles.limit); + } + } + }; + + const handleChange = ( + event: React.SyntheticEvent, + value: any[] + ): void => { + setSelectedRoles(value); + }; + + useEffect(() => { + if (!selectedRoles) return; + handleSelectedUsers(selectedRoles); + }, [selectedRoles]); + + return ( + option.name} + label={'Select Role(s)'} + multiple={true} + disabled={isDisabled} + value={selectedRoles} + onChange={handleChange} + onInputChange={handleSearchChange} + disableCloseOnSelect + renderTags={(value, getTagProps) => + value.map((option, index) => ( + + )) + } + textFieldProps={{ variant: 'filled' }} + ListboxProps={{ + onScroll: handleScroll, + style: { + maxHeight: '280px', + overflow: 'auto', + }, + }} + /> + ); +} + +export default TcRolesAutoComplete; diff --git a/src/components/announcements/create/privateMessaageContainer/TcUsersAutoComplete.tsx b/src/components/announcements/create/privateMessaageContainer/TcUsersAutoComplete.tsx new file mode 100644 index 00000000..07d422f3 --- /dev/null +++ b/src/components/announcements/create/privateMessaageContainer/TcUsersAutoComplete.tsx @@ -0,0 +1,169 @@ +import React, { useEffect, useState } from 'react'; +import { useToken } from '../../../../context/TokenContext'; +import useAppStore from '../../../../store/useStore'; +import { FetchedData, IUser } from '../../../../utils/interfaces'; +import { debounce } from '../../../../helpers/helper'; +import TcAutocomplete from '../../../shared/TcAutocomplete'; +import { Chip } from '@mui/material'; + +interface ITcUsersAutoCompleteProps { + isDisabled: boolean; + handleSelectedUsers: (users: IUser[]) => void; +} + +function TcUsersAutoComplete({ + isDisabled, + handleSelectedUsers, +}: ITcUsersAutoCompleteProps) { + const { community } = useToken(); + + const platformId = community?.platforms.find( + (platform) => platform.disconnectedAt === null + )?.id; + + const { retrievePlatformProperties } = useAppStore(); + const [selectedUsers, setSelectedUsers] = useState([]); + + const [fetchedUsers, setFetchedUsers] = useState({ + limit: 8, + page: 1, + results: [], + totalPages: 0, + totalResults: 0, + }); + const [filteredRolesByName, setFilteredRolesByName] = useState(''); + + const fetchDiscordUsers = async ( + platformId: string, + page?: number, + limit?: number, + name?: string + ) => { + try { + const fetchedUsers = await retrievePlatformProperties({ + platformId, + name: name, + property: 'guildMember', + page: page, + limit: limit, + }); + + if (name) { + setFilteredRolesByName(name); + setFetchedUsers(fetchedUsers); + } else { + setFetchedUsers((prevData: { results: any }) => { + const updatedResults = [ + ...prevData.results, + ...fetchedUsers.results, + ].filter( + (role, index, self) => + index === self.findIndex((r) => r.discordId === role.discordId) + ); + + return { + ...prevData, + ...fetchedUsers, + results: updatedResults, + }; + }); + } + } catch (error) {} + }; + + useEffect(() => { + if (!platformId) return; + fetchDiscordUsers(platformId, fetchedUsers.page, fetchedUsers.limit); + }, []); + + const debouncedFetchDiscordUsers = debounce(fetchDiscordUsers, 700); + + const handleSearchChange = (event: React.SyntheticEvent) => { + const target = event.target as HTMLInputElement; + const inputValue = target.value; + + if (!platformId) return; + + if (inputValue === '') { + setFilteredRolesByName(''); + setFetchedUsers({ + limit: 8, + page: 1, + results: [], + totalPages: 0, + totalResults: 0, + }); + + debouncedFetchDiscordUsers(platformId, 1, 8); + } else { + debouncedFetchDiscordUsers(platformId, 1, 8, inputValue); + } + }; + + const handleScroll = (event: React.UIEvent) => { + const listboxNode = event.currentTarget; + if ( + listboxNode.scrollTop + listboxNode.clientHeight === + listboxNode.scrollHeight + ) { + const nextPage = + Math.ceil(fetchedUsers.results.length / fetchedUsers.limit) + 1; + if (fetchedUsers.totalPages >= nextPage) { + if (!platformId) return; + fetchDiscordUsers(platformId, nextPage, fetchedUsers.limit); + } + } + }; + + const handleChange = ( + event: React.SyntheticEvent, + value: any[] + ): void => { + setSelectedUsers(value); + }; + + useEffect(() => { + if (!selectedUsers) return; + handleSelectedUsers(selectedUsers); + }, [selectedUsers]); + + return ( + option.ngu} + label={'Select Role(s)'} + multiple={true} + disabled={isDisabled} + value={selectedUsers} + onChange={handleChange} + onInputChange={handleSearchChange} + disableCloseOnSelect + renderTags={(value, getTagProps) => + value.map((option, index) => ( + + )) + } + textFieldProps={{ variant: 'filled' }} + ListboxProps={{ + onScroll: handleScroll, + style: { + maxHeight: '280px', + overflow: 'auto', + }, + }} + /> + ); +} + +export default TcUsersAutoComplete; diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.tsx index 34116b48..7c55c44c 100644 --- a/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.tsx +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.tsx @@ -8,7 +8,6 @@ import { AccordionDetails, AccordionSummary, FormControl, - FormHelperText, InputLabel, } from '@mui/material'; import TcInput from '../../../shared/TcInput'; diff --git a/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx index ba731a62..fcdb58fe 100644 --- a/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx +++ b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx @@ -55,13 +55,19 @@ function TcScheduleAnnouncement({ }; useEffect(() => { - if (!selectedTime) return; + if (!selectedDate || !selectedTime) return; - const formattedTime = selectedTime.toISOString(); - console.log({ formattedTime }); + const fullDateTime = moment(selectedDate).set({ + hour: selectedTime.getHours(), + minute: selectedTime.getMinutes(), + }); - handleSchaduledDate({ selectedTime: formattedTime }); - }, [selectedTime]); + const fullDateTimeUTC = fullDateTime.utc(); + + const formattedUTCDate = fullDateTimeUTC.format(); + + handleSchaduledDate({ selectedTime: formattedUTCDate }); + }, [selectedDate, selectedTime]); return (
diff --git a/src/components/shared/TcAutocomplete.tsx b/src/components/shared/TcAutocomplete.tsx new file mode 100644 index 00000000..287497ea --- /dev/null +++ b/src/components/shared/TcAutocomplete.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import Autocomplete, { AutocompleteProps } from '@mui/material/Autocomplete'; +import TextField, { TextFieldProps } from '@mui/material/TextField'; + +interface TcAutocompleteProps + extends Omit, 'renderInput'> { + label?: string; + placeholder?: string; + textFieldProps?: TextFieldProps; +} + +function TcAutocomplete({ + options, + label, + placeholder, + textFieldProps, + ...props +}: TcAutocompleteProps) { + return ( + ( + + )} + {...props} + /> + ); +} + +export default TcAutocomplete; diff --git a/src/context/ChannelContext.tsx b/src/context/ChannelContext.tsx index a789f5e8..442188a4 100644 --- a/src/context/ChannelContext.tsx +++ b/src/context/ChannelContext.tsx @@ -30,7 +30,8 @@ interface ChannelContextProps { platformId: string, property?: 'channel', selectedChannels?: string[], - hideDeactiveSubchannels?: boolean + hideDeactiveSubchannels?: boolean, + allDefaultChecked?: boolean ) => Promise; handleSubChannelChange: (channelId: string, subChannelId: string) => void; handleSelectAll: (channelId: string, subChannels: SubChannel[]) => void; @@ -57,7 +58,8 @@ const initialChannelContextData: ChannelContextProps = { platformId: string, property?: 'channel', selectedChannels?: string[], - hideDeactiveSubchannels?: boolean + hideDeactiveSubchannels?: boolean, + allDefaultChecked?: boolean ) => {}, handleSubChannelChange: (channelId: string, subChannelId: string) => {}, handleSelectAll: (channelId: string, subChannels: SubChannel[]) => {}, @@ -84,7 +86,8 @@ export const ChannelProvider = ({ children }: ChannelProviderProps) => { platformId: string, property: 'channel' = 'channel', selectedChannels?: string[], - hideDeactiveSubchannels: boolean = false + hideDeactiveSubchannels: boolean = false, + allDefaultChecked: boolean = true ) => { setLoading(true); try { @@ -101,8 +104,13 @@ export const ChannelProvider = ({ children }: ChannelProviderProps) => { (acc: any, channel: any) => { acc[channel.channelId] = channel.subChannels.reduce( (subAcc: any, subChannel: any) => { - subAcc[subChannel.channelId] = - subChannel.canReadMessageHistoryAndViewChannel; + if (allDefaultChecked) { + subAcc[subChannel.channelId] = + subChannel.canReadMessageHistoryAndViewChannel; + } else { + subAcc[subChannel.channelId] = false; + } + return subAcc; }, {} as { [subChannelId: string]: boolean } diff --git a/src/pages/announcements/create-new-announcements.tsx b/src/pages/announcements/create-new-announcements.tsx index 9298382b..233dd6ed 100644 --- a/src/pages/announcements/create-new-announcements.tsx +++ b/src/pages/announcements/create-new-announcements.tsx @@ -3,7 +3,7 @@ import { defaultLayout } from '../../layouts/defaultLayout'; import SEO from '../../components/global/SEO'; import TcBoxContainer from '../../components/shared/TcBox/TcBoxContainer'; import TcPublicMessageContainer from '../../components/announcements/create/publicMessageContainer/TcPublicMessageContainer'; -import TcPrivateMessaageContainer from '../../components/announcements/create/privateMessaageContainer'; +import TcPrivateMessageContainer from '../../components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer'; import TcButton from '../../components/shared/TcButton'; import TcScheduleAnnouncement from '../../components/announcements/create/scheduleAnnouncement/'; import TcSelectPlatform from '../../components/announcements/create/selectPlatform'; @@ -13,6 +13,7 @@ import TcConfirmSchaduledAnnouncementsDialog from '../../components/announcement import useAppStore from '../../store/useStore'; import { useToken } from '../../context/TokenContext'; import { ChannelContext } from '../../context/ChannelContext'; +import { IRoles, IUser } from '../../utils/interfaces'; export type CreateAnnouncementsPayloadDataOptions = | { channelIds: string[]; userIds?: string[]; roleIds?: string[] } @@ -41,25 +42,27 @@ function CreateNewAnnouncements() { const { refreshData } = channelContext; + const [channels, setChannels] = useState([]); + const [roles, setRoles] = useState([]); + const [users, setUsers] = useState([]); + const platformId = community?.platforms.find( (platform) => platform.disconnectedAt === null )?.id; const [publicAnnouncements, setPublicAnnouncements] = useState(); + + const [privateAnnouncements, setPrivateAnnouncements] = + useState(); + const [scheduledAt, setScheduledAt] = useState(); const fetchPlatformChannels = async () => { try { if (platformId) { - const data = await retrievePlatformById(platformId); - const { metadata } = data; - if (metadata) { - const { selectedChannels } = metadata; - await refreshData(platformId, 'channel', selectedChannels, true); - } else { - await refreshData(platformId); - } + await retrievePlatformById(platformId); + await refreshData(platformId, 'channel', undefined, undefined, false); } } catch (error) { } finally { @@ -76,15 +79,24 @@ function CreateNewAnnouncements() { const handleCreateAnnouncements = (isDrafted: boolean) => { if (!community) return; + + const data = [publicAnnouncements]; + + if (privateAnnouncements && privateAnnouncements.length > 0) { + data.push(...privateAnnouncements); + } + const announcementsPayload = { communityId: community.id, draft: isDrafted, scheduledAt: scheduledAt, - data: [publicAnnouncements], + data: data, }; + console.log({ announcementsPayload }); createNewAnnouncements(announcementsPayload); }; + return ( <> @@ -106,6 +118,7 @@ function CreateNewAnnouncements() { selectedChannels, }) => { if (!platformId) return; + setChannels(selectedChannels); setPublicAnnouncements({ platformId: platformId, template: message, @@ -117,14 +130,63 @@ function CreateNewAnnouncements() { }); }} /> - + { + if (!platformId) return; + + let rolePrivateAnnouncements; + let userPrivateAnnouncements; + + const commonData = { + platformId: platformId, + template: message, + }; + + if (selectedRoles && selectedRoles.length > 0) { + setRoles(selectedRoles); + + rolePrivateAnnouncements = { + ...commonData, + options: { + roleIds: selectedRoles.map((role) => + role.id.toString() + ), + }, + }; + } + + if (selectedUsers && selectedUsers.length > 0) { + setUsers(selectedUsers); + + userPrivateAnnouncements = { + ...commonData, + options: { + userIds: selectedUsers.map((user) => user.discordId), + }, + }; + } + + const announcements = []; + if (rolePrivateAnnouncements) + announcements.push(rolePrivateAnnouncements); + if (userPrivateAnnouncements) + announcements.push(userPrivateAnnouncements); + + setPrivateAnnouncements(announcements); + }} + /> + { setScheduledAt(selectedTime); }} />
-
+
router.push('/announcements')} @@ -150,8 +212,18 @@ function CreateNewAnnouncements() { /> + handleCreateAnnouncements(e) + } />
diff --git a/src/store/types/IPlatform.ts b/src/store/types/IPlatform.ts index 6ad1e5f5..c03294de 100644 --- a/src/store/types/IPlatform.ts +++ b/src/store/types/IPlatform.ts @@ -14,7 +14,7 @@ export interface IRetrivePlatformRolesOrChannels { sortBy?: string; name?: string; platformId: string; - property: 'channel' | 'role'; + property: 'channel' | 'role' | 'guildMember'; } export interface IDeletePlatformProps { diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index d8218423..543828b3 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -171,3 +171,12 @@ export interface IDiscordModifiedCommunity extends Omit { platforms: ICommunityDiscordPlatfromProps[]; } + +export interface IUser { + discordId: string; + discriminator: string; + globalName: string | null; + ngu: string; + nickname: string | null; + username: string; +} From ed036731a623594809d664a865e9b84b9420cca3 Mon Sep 17 00:00:00 2001 From: zuies Date: Wed, 17 Jan 2024 17:39:41 +0300 Subject: [PATCH 22/30] add list integration --- .../announcements/TcAnnouncementsTable.tsx | 256 ++++++++++++++++++ src/components/announcements/TcTimeZone.tsx | 11 +- .../TcUsersAutoComplete.tsx | 2 +- .../TcScheduleAnnouncement.tsx | 2 +- src/components/shared/TcDatePickerPopover.tsx | 44 +++ .../create-new-announcements.tsx | 26 +- .../announcements/edit-announcements.tsx | 60 ---- .../edit-announcements/index.tsx | 102 +++++++ src/pages/announcements/index.tsx | 158 ++++++----- src/store/slices/announcementsSlice.ts | 8 +- src/store/types/IAnnouncements.ts | 3 + 11 files changed, 517 insertions(+), 155 deletions(-) create mode 100644 src/components/announcements/TcAnnouncementsTable.tsx create mode 100644 src/components/shared/TcDatePickerPopover.tsx delete mode 100644 src/pages/announcements/edit-announcements.tsx create mode 100644 src/pages/announcements/edit-announcements/index.tsx diff --git a/src/components/announcements/TcAnnouncementsTable.tsx b/src/components/announcements/TcAnnouncementsTable.tsx new file mode 100644 index 00000000..ff9a8118 --- /dev/null +++ b/src/components/announcements/TcAnnouncementsTable.tsx @@ -0,0 +1,256 @@ +import React, { useState } from 'react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, + IconButton, + MenuItem, + Menu, +} from '@mui/material'; +import { BsThreeDotsVertical } from 'react-icons/bs'; +import Router from 'next/router'; +import TcDialog from '../shared/TcDialog'; +import TcButton from '../shared/TcButton'; +import { AiOutlineClose } from 'react-icons/ai'; +import TcText from '../shared/TcText'; +import useAppStore from '../../store/useStore'; +import { useSnackbar } from '../../context/SnackbarContext'; +import { MdModeEdit, MdDelete } from 'react-icons/md'; + +interface Channel { + channelId: string; + name: string; +} + +interface User { + discordId: string; + ngu: string; +} + +interface Role { + roleId: string; + color: string; + name: string; +} + +interface AnnouncementData { + platform: string; + template: string; + options: { + channels: Channel[]; + users?: User[]; + roles?: Role[]; + }; +} + +interface Announcement { + id: string; + title: string; + scheduledAt: string; + draft: boolean; + data: AnnouncementData[]; + community: string; +} + +interface AnnouncementsTableProps { + announcements: Announcement[]; + handleRefreshList: () => void; +} + +function TcAnnouncementsTable({ + announcements, + handleRefreshList, +}: AnnouncementsTableProps) { + const { deleteAnnouncements } = useAppStore(); + const { showMessage } = useSnackbar(); + const [anchorEl, setAnchorEl] = useState(null); + const [deleteConfirmDialogOpen, setDeleteConfirmDialogOpen] = + useState(false); + const [selectedAnnouncementId, setSelectedAnnouncementId] = useState< + string | null + >(null); + + const handleClick = ( + event: React.MouseEvent, + id: string + ) => { + setAnchorEl(event.currentTarget); + setSelectedAnnouncementId(id); + }; + + const handleClose = () => { + setAnchorEl(null); + setSelectedAnnouncementId(null); + }; + + const handleEdit = (id: string) => { + Router.push(`/announcements/edit-announcements/?announcementsId=${id}`); + handleClose(); + }; + + const handleDelete = () => { + setDeleteConfirmDialogOpen(true); + }; + + const handleDeleteAnnouncements = async (id: string) => { + try { + const data = await deleteAnnouncements(id); + + handleRefreshList(); + showMessage('Scheduled announcement removed successfully.', 'success'); + } catch (error) { + console.error('Error deleting announcement:', error); + showMessage('Error occurred while deleting the announcement.', 'error'); + } finally { + setDeleteConfirmDialogOpen(false); + setSelectedAnnouncementId(null); + } + }; + + return ( + <> + + + + Announcements + Channels + Handle + Role + Date + + + + + {announcements.map((announcement, index) => ( + + {announcement.data[0].template} + + {announcement.data.map( + (item) => + item.options.channels && + item.options.channels + .map((channel) => `#${channel.name}`) + .join(', ') + )} + + + {announcement.data.map((item) => + item.options.users + ? item.options.users + .map((user) => `@${user.ngu}`) + .join(', ') + : '' + )} + + + {announcement.data.map((item) => + item.options.roles + ? item.options.roles.map((role) => role.name).join(', ') + : '' + )} + + + {new Date(announcement.scheduledAt).toLocaleString()} + {' '} + + handleClick(event, announcement.id)} + > + + + + handleEdit(announcement.id)}> + + Edit + + + + Delete + + + + + ))} + +
+ +
+ setDeleteConfirmDialogOpen(false)} + /> +
+
+ +
+ +
+ setDeleteConfirmDialogOpen(false)} + /> + + selectedAnnouncementId && + handleDeleteAnnouncements(selectedAnnouncementId) + } + /> +
+
+
+ + } + open={deleteConfirmDialogOpen} + /> + + ); +} + +export default TcAnnouncementsTable; diff --git a/src/components/announcements/TcTimeZone.tsx b/src/components/announcements/TcTimeZone.tsx index d37e357a..180f7af8 100644 --- a/src/components/announcements/TcTimeZone.tsx +++ b/src/components/announcements/TcTimeZone.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import TcButton from '../shared/TcButton'; import { FaGlobeAmericas } from 'react-icons/fa'; import TcPopover from '../shared/TcPopover'; @@ -12,7 +12,10 @@ import { MdSearch } from 'react-icons/md'; const timeZonesList = momentTZ.tz.names(); -function TcTimeZone() { +interface ITcTimeZoneProps { + handleZone: (zone: string) => void; +} +function TcTimeZone({ handleZone }: ITcTimeZoneProps) { const [activeZone, setActiveZone] = useState(moment.tz.guess()); const [anchorEl, setAnchorEl] = useState(null); @@ -47,6 +50,10 @@ function TcTimeZone() { setZones(timeZonesList); }; + useEffect(() => { + handleZone(activeZone); + }, [activeZone]); + return (
option.ngu} - label={'Select Role(s)'} + label={'Select User(s)'} multiple={true} disabled={isDisabled} value={selectedUsers} diff --git a/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx index fcdb58fe..52c19986 100644 --- a/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx +++ b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx @@ -18,7 +18,7 @@ function TcScheduleAnnouncement({ const [selectedDate, setSelectedDate] = useState(null); const [selectedTime, setSelectedTime] = useState(null); const [dateTimeDisplay, setDateTimeDisplay] = useState( - moment().format('D MMMM YYYY @ h A') + 'Select Date for Announcement' ); const handleOpen = (event: React.MouseEvent) => { diff --git a/src/components/shared/TcDatePickerPopover.tsx b/src/components/shared/TcDatePickerPopover.tsx new file mode 100644 index 00000000..d323a047 --- /dev/null +++ b/src/components/shared/TcDatePickerPopover.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import Popover from '@mui/material/Popover'; +import { StaticDatePicker } from '@mui/x-date-pickers/StaticDatePicker'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; + +interface ITcDatePickerPopoverProps { + open: boolean; + anchorEl: HTMLElement | null; + onClose: () => void; + selectedDate: Date | null; + onDateChange: (date: Date | null) => void; +} + +function TcDatePickerPopover({ + open, + anchorEl, + onClose, + selectedDate, + onDateChange, +}: ITcDatePickerPopoverProps) { + return ( + + + + + + ); +} + +export default TcDatePickerPopover; diff --git a/src/pages/announcements/create-new-announcements.tsx b/src/pages/announcements/create-new-announcements.tsx index 233dd6ed..97df6983 100644 --- a/src/pages/announcements/create-new-announcements.tsx +++ b/src/pages/announcements/create-new-announcements.tsx @@ -8,12 +8,13 @@ import TcButton from '../../components/shared/TcButton'; import TcScheduleAnnouncement from '../../components/announcements/create/scheduleAnnouncement/'; import TcSelectPlatform from '../../components/announcements/create/selectPlatform'; import TcBreadcrumbs from '../../components/shared/TcBreadcrumbs'; -import router from 'next/router'; import TcConfirmSchaduledAnnouncementsDialog from '../../components/announcements/TcConfirmSchaduledAnnouncementsDialog'; import useAppStore from '../../store/useStore'; import { useToken } from '../../context/TokenContext'; import { ChannelContext } from '../../context/ChannelContext'; import { IRoles, IUser } from '../../utils/interfaces'; +import { useSnackbar } from '../../context/SnackbarContext'; +import { useRouter } from 'next/router'; export type CreateAnnouncementsPayloadDataOptions = | { channelIds: string[]; userIds?: string[]; roleIds?: string[] } @@ -34,11 +35,13 @@ export interface CreateAnnouncementsPayload { } function CreateNewAnnouncements() { + const router = useRouter(); const { createNewAnnouncements, retrievePlatformById } = useAppStore(); const { community } = useToken(); const channelContext = useContext(ChannelContext); + const { showMessage } = useSnackbar(); const { refreshData } = channelContext; @@ -77,7 +80,7 @@ function CreateNewAnnouncements() { fetchPlatformChannels(); }, [platformId]); - const handleCreateAnnouncements = (isDrafted: boolean) => { + const handleCreateAnnouncements = async (isDrafted: boolean) => { if (!community) return; const data = [publicAnnouncements]; @@ -92,9 +95,16 @@ function CreateNewAnnouncements() { scheduledAt: scheduledAt, data: data, }; - console.log({ announcementsPayload }); - createNewAnnouncements(announcementsPayload); + try { + const data = await createNewAnnouncements(announcementsPayload); + if (data) { + showMessage('Announcement created successfully', 'success'); + router.push('/announcements'); + } + } catch (error) { + showMessage('Failed to create announcement', 'error'); + } }; return ( @@ -153,7 +163,7 @@ function CreateNewAnnouncements() { ...commonData, options: { roleIds: selectedRoles.map((role) => - role.id.toString() + role.roleId.toString() ), }, }; @@ -216,11 +226,7 @@ function CreateNewAnnouncements() { selectedRoles={roles} selectedUsernames={users} schaduledDate={scheduledAt || ''} - isDisabled={ - !scheduledAt || - !publicAnnouncements?.template || - !publicAnnouncements?.options.channelIds - } + isDisabled={!scheduledAt} handleCreateAnnouncements={(e) => handleCreateAnnouncements(e) } diff --git a/src/pages/announcements/edit-announcements.tsx b/src/pages/announcements/edit-announcements.tsx deleted file mode 100644 index ee711383..00000000 --- a/src/pages/announcements/edit-announcements.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import { defaultLayout } from '../../layouts/defaultLayout'; -import SEO from '../../components/global/SEO'; -import router from 'next/router'; -import TcPrivateMessaageContainer from '../../components/announcements/create/privateMessaageContainer'; -import TcPublicMessaageContainer from '../../components/announcements/create/publicMessageContainer'; -import TcScheduleAnnouncement from '../../components/announcements/create/scheduleAnnouncement'; -import TcSelectPlatform from '../../components/announcements/create/selectPlatform'; -import TcBoxContainer from '../../components/shared/TcBox/TcBoxContainer'; -import TcBreadcrumbs from '../../components/shared/TcBreadcrumbs'; -import TcButton from '../../components/shared/TcButton'; -import TcConfirmSchaduledAnnouncementsDialog from '../../components/announcements/TcConfirmSchaduledAnnouncementsDialog'; - -function EditAnnouncements() { - return ( - <> - -
- - -
- - - - -
-
- router.push('/announcements')} - variant="outlined" - sx={{ - maxWidth: { - xs: '100%', - sm: '8rem', - }, - }} - /> -
- -
-
-
- } - /> -
- - ); -} - -EditAnnouncements.pageLayout = defaultLayout; - -export default EditAnnouncements; diff --git a/src/pages/announcements/edit-announcements/index.tsx b/src/pages/announcements/edit-announcements/index.tsx new file mode 100644 index 00000000..cd74aa84 --- /dev/null +++ b/src/pages/announcements/edit-announcements/index.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { defaultLayout } from '../../../layouts/defaultLayout'; +import SEO from '../../../components/global/SEO'; +import router from 'next/router'; +import TcPrivateMessaageContainer from '../../../components/announcements/create/privateMessaageContainer'; +import TcPublicMessaageContainer from '../../../components/announcements/create/publicMessageContainer'; +import TcScheduleAnnouncement from '../../../components/announcements/create/scheduleAnnouncement'; +import TcSelectPlatform from '../../../components/announcements/create/selectPlatform'; +import TcBoxContainer from '../../../components/shared/TcBox/TcBoxContainer'; +import TcBreadcrumbs from '../../../components/shared/TcBreadcrumbs'; +import TcButton from '../../../components/shared/TcButton'; +import TcConfirmSchaduledAnnouncementsDialog from '../../../components/announcements/TcConfirmSchaduledAnnouncementsDialog'; +import { FlattenedChannel } from '../../../components/announcements/create/publicMessageContainer/TcPublicMessageContainer'; +import { IRoles, IUser } from '../../../utils/interfaces'; + +function Index() { + return ( + <> + +
+ + +
+ + + + +
+
+ router.push('/announcements')} + variant="outlined" + sx={{ + maxWidth: { + xs: '100%', + sm: '8rem', + }, + }} + /> +
+ +
+
+
+ } + /> +
+ + ); +} + +Index.pageLayout = defaultLayout; + +export default Index; diff --git a/src/pages/announcements/index.tsx b/src/pages/announcements/index.tsx index c56675d7..58ecfcd4 100644 --- a/src/pages/announcements/index.tsx +++ b/src/pages/announcements/index.tsx @@ -5,18 +5,16 @@ import SEO from '../../components/global/SEO'; import TcText from '../../components/shared/TcText'; import TcButton from '../../components/shared/TcButton'; import { BsPlus } from 'react-icons/bs'; -import TcTableContainer from '../../components/shared/TcTableContainer'; import router from 'next/router'; import TcPagination from '../../components/shared/TcPagination'; import TcTimeZone from '../../components/announcements/TcTimeZone'; -import TcDateTimePopover from '../../components/announcements/create/scheduleAnnouncement/TcDateTimePopover'; import moment from 'moment'; import { MdCalendarMonth } from 'react-icons/md'; import useAppStore from '../../store/useStore'; import { StorageService } from '../../services/StorageService'; import { FetchedData, IDiscordModifiedCommunity } from '../../utils/interfaces'; - -const headers = ['Announcement', 'Channel', 'Handle', 'Role', 'Date']; +import TcAnnouncementsTable from '../../components/announcements/TcAnnouncementsTable'; +import TcDatePickerPopover from '../../components/shared/TcDatePickerPopover'; function Index() { const { retrieveAnnouncements } = useAppStore(); @@ -27,15 +25,15 @@ function Index() { const [anchorEl, setAnchorEl] = useState(null); const [activeTab, setActiveTab] = useState(0); const [selectedDate, setSelectedDate] = useState(null); - const [selectedTime, setSelectedTime] = useState(null); - const [dateTimeDisplay, setDateTimeDisplay] = useState( - moment().format('D MMMM YYYY @ h A') - ); + const [selectedZone, setSelectedZone] = useState(moment.tz.guess()); + const [dateTimeDisplay, setDateTimeDisplay] = useState('Filter Date'); const [loading, setLoading] = useState(false); + const [page, setPage] = useState(1); + const [fetchedAnnouncements, setFetchedAnnouncements] = useState( { - limit: 10, + limit: 8, page: 1, results: [], totalPages: 0, @@ -43,10 +41,6 @@ function Index() { } ); - const handleOpen = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - const handleClose = () => { setAnchorEl(null); }; @@ -54,61 +48,63 @@ function Index() { const open = Boolean(anchorEl); const id = open ? 'date-time-popover' : undefined; + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleDateChange = (date: Date | null) => { if (date) { setSelectedDate(date); + const fullDateTime = moment(date); + setDateTimeDisplay(fullDateTime.format('D MMMM YYYY')); + setActiveTab(1); + setAnchorEl(null); } }; - const handleTimeChange = (time: Date | null) => { - if (time) { - setSelectedTime(time); - handleClose(); - - if (selectedDate) { - const fullDateTime = moment(selectedDate).set({ - hour: time.getHours(), - minute: time.getMinutes(), - }); - setDateTimeDisplay(fullDateTime.format('D MMMM YYYY @ h A')); + const fetchData = async (date?: Date | null, zone?: string) => { + try { + setLoading(true); + + let startDate, endDate; + if (date) { + startDate = moment(date) + .tz(zone || selectedZone) + .startOf('day') + .utcOffset(0, true) + .format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'); + endDate = moment(date) + .tz(zone || selectedZone) + .endOf('day') + .utcOffset(0, true) + .format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'); } + const data = await retrieveAnnouncements({ + page: page, + limit: 8, + timeZone: zone || selectedZone, + ...(startDate ? { startDate: startDate } : {}), + ...(endDate ? { endDate: endDate } : {}), + community: communityId, + }); + + setFetchedAnnouncements(data); + } catch (error) { + console.error('An error occurred:', error); + } finally { + setLoading(false); } }; useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - - const data = await retrieveAnnouncements({ - page: 1, - limit: 10, - community: communityId, - }); - - setFetchedAnnouncements(data); - setLoading(false); - } catch (error) { - console.error('An error occurred while fetching platforms:', error); - setLoading(false); - } - }; - - fetchData(); - }, []); - - const formatAnnouncementsForTable = () => { - console.log(fetchedAnnouncements.results); + console.log({ selectedDate }); - return fetchedAnnouncements.results.map( - (announcement) => console.log(announcement.data.options) + fetchData(selectedDate, selectedZone); + }, [selectedZone, selectedDate, page]); - // { - // Announcement: announcement.title, - // Date: moment(announcement.scheduledAt).format('YYYY-MM-DD'), - // } - ); + const handlePageChange = (selectedPage: number) => { + setPage(selectedPage); }; return ( @@ -132,32 +128,29 @@ function Index() {
} - disableElevation={true} - className="border border-black bg-gray-100 shadow-md" - sx={{ color: 'black', height: '2.4rem' }} + onClick={handleClick} + text={dateTimeDisplay} aria-describedby={id} - onClick={handleOpen} /> - - +
{fetchedAnnouncements.results.length > 0 ? ( - ) : (
@@ -175,16 +168,21 @@ function Index() { )}
-
- -
+ {fetchedAnnouncements.totalResults > 8 ? ( +
+ +
+ ) : ( + '' + )}
} /> diff --git a/src/store/slices/announcementsSlice.ts b/src/store/slices/announcementsSlice.ts index 96595163..5ef14cbf 100644 --- a/src/store/slices/announcementsSlice.ts +++ b/src/store/slices/announcementsSlice.ts @@ -11,6 +11,9 @@ const createAnnouncementsSlice: StateCreator = (set, get) => ({ limit, sortBy, name, + timeZone, + startDate, + endDate, community, }: IRetrieveAnnouncementsProps) => { try { @@ -18,6 +21,9 @@ const createAnnouncementsSlice: StateCreator = (set, get) => ({ page, limit, sortBy, + ...(timeZone ? { timeZone } : {}), + ...(startDate ? { startDate } : {}), + ...(endDate ? { endDate } : {}), ...(name ? { name } : {}), }; @@ -62,7 +68,7 @@ const createAnnouncementsSlice: StateCreator = (set, get) => ({ }, deleteAnnouncements: async (id: string) => { try { - const { data } = await axiosInstance.delete(`/platforms/${id}`); + const { data } = await axiosInstance.delete(`/announcements/${id}`); return data; } catch (error) { console.error('Failed to delete announcements:', error); diff --git a/src/store/types/IAnnouncements.ts b/src/store/types/IAnnouncements.ts index 916030ae..e631ae48 100644 --- a/src/store/types/IAnnouncements.ts +++ b/src/store/types/IAnnouncements.ts @@ -4,6 +4,9 @@ export interface IRetrieveAnnouncementsProps { sortBy?: string; name?: string; community: string; + startDate?: string; + endDate?: string; + timeZone: string; } export default interface IAnnouncements { From 7926de5bd60ef644dff7440890a9f2b964448ef9 Mon Sep 17 00:00:00 2001 From: zuies Date: Thu, 18 Jan 2024 10:54:44 +0300 Subject: [PATCH 23/30] update create,list announcements --- .../announcements/TcAnnouncementsTable.tsx | 92 ++++++++++++++++--- src/components/shared/TcDatePickerPopover.tsx | 11 +++ .../create-new-announcements.tsx | 1 + src/pages/announcements/index.tsx | 9 +- 4 files changed, 99 insertions(+), 14 deletions(-) diff --git a/src/components/announcements/TcAnnouncementsTable.tsx b/src/components/announcements/TcAnnouncementsTable.tsx index ff9a8118..122d3ffa 100644 --- a/src/components/announcements/TcAnnouncementsTable.tsx +++ b/src/components/announcements/TcAnnouncementsTable.tsx @@ -114,12 +114,76 @@ function TcAnnouncementsTable({ - Announcements - Channels - Handle - Role - Date - + + + + + + + + + + + + + + + + @@ -132,8 +196,10 @@ function TcAnnouncementsTable({ : 'border-1 border-solid border-gray-700' }`} > - {announcement.data[0].template} - + + {announcement.data[0].template} + + {announcement.data.map( (item) => item.options.channels && @@ -142,7 +208,7 @@ function TcAnnouncementsTable({ .join(', ') )} - + {announcement.data.map((item) => item.options.users ? item.options.users @@ -151,17 +217,17 @@ function TcAnnouncementsTable({ : '' )} - + {announcement.data.map((item) => item.options.roles ? item.options.roles.map((role) => role.name).join(', ') : '' )} - + {new Date(announcement.scheduledAt).toLocaleString()} - {' '} - + + void; selectedDate: Date | null; onDateChange: (date: Date | null) => void; + onResetDate: () => void; } function TcDatePickerPopover({ @@ -18,6 +20,7 @@ function TcDatePickerPopover({ onClose, selectedDate, onDateChange, + onResetDate, }: ITcDatePickerPopoverProps) { return ( +
+ +
); } diff --git a/src/pages/announcements/create-new-announcements.tsx b/src/pages/announcements/create-new-announcements.tsx index 97df6983..60aa02cc 100644 --- a/src/pages/announcements/create-new-announcements.tsx +++ b/src/pages/announcements/create-new-announcements.tsx @@ -212,6 +212,7 @@ function CreateNewAnnouncements() { { + setSelectedDate(null); + setDateTimeDisplay('Filter Date'); + + setAnchorEl(null); + }; + const fetchData = async (date?: Date | null, zone?: string) => { try { setLoading(true); @@ -140,6 +146,7 @@ function Index() { onClose={handleClose} selectedDate={selectedDate} onDateChange={handleDateChange} + onResetDate={resetDateFilter} /> From 8242779b6cf63dec19e1f9d866a9dfd2c4a4bbcd Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 19 Jan 2024 00:19:55 +0300 Subject: [PATCH 24/30] integrate edit announcements --- .../TcConfirmSchaduledAnnouncementsDialog.tsx | 18 +- .../TcPrivateMessaageContainer.tsx | 50 +++- .../TcRolesAutoComplete.tsx | 16 ++ .../TcUsersAutoComplete.tsx | 24 +- .../TcPublicMessageContainer.tsx | 23 ++ .../TcScheduleAnnouncement.tsx | 13 + .../pages/pageIndex/HeatmapChart.tsx | 10 + .../edit-announcements/index.tsx | 269 ++++++++++++++---- src/pages/announcements/index.tsx | 2 - src/store/slices/announcementsSlice.ts | 16 +- src/store/types/IAnnouncements.ts | 4 +- src/utils/interfaces.ts | 12 +- 12 files changed, 387 insertions(+), 70 deletions(-) diff --git a/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx index b411c4f2..7e7c9cec 100644 --- a/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx +++ b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx @@ -94,7 +94,11 @@ function TcConfirmSchaduledAnnouncementsDialog({ className="text-left text-gray-400" /> @@ -112,7 +116,11 @@ function TcConfirmSchaduledAnnouncementsDialog({ className="text-left text-gray-400" /> {' '} @@ -133,7 +141,11 @@ function TcConfirmSchaduledAnnouncementsDialog({ className="text-left text-gray-400" /> diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx index 78058436..e97ddcd6 100644 --- a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx +++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx @@ -13,6 +13,10 @@ import TcPrivateMessagePreviewDialog from './TcPrivateMessagePreviewDialog'; import TcRolesAutoComplete from './TcRolesAutoComplete'; import TcUsersAutoComplete from './TcUsersAutoComplete'; import { IRoles, IUser } from '../../../../utils/interfaces'; +import { + DiscordData, + DiscordPrivateOptions, +} from '../../../../pages/announcements/edit-announcements'; export enum MessageType { Both = 'Both', @@ -21,6 +25,8 @@ export enum MessageType { } export interface ITcPrivateMessageContainerProps { + isEdit?: boolean; + privateAnnouncementsData?: DiscordData[] | undefined; handlePrivateAnnouncements: ({ message, selectedRoles, @@ -34,6 +40,8 @@ export interface ITcPrivateMessageContainerProps { function TcPrivateMessageContainer({ handlePrivateAnnouncements, + isEdit = false, + privateAnnouncementsData, }: ITcPrivateMessageContainerProps) { const [privateMessage, setPrivateMessage] = useState(false); const [messageType, setMessageType] = useState(MessageType.Both); @@ -101,6 +109,37 @@ function TcPrivateMessageContainer({ handlePrivateAnnouncements, ]); + useEffect(() => { + if (isEdit && privateAnnouncementsData) { + const rolesArray: IRoles[] = []; + const usersArray: IUser[] = []; + let templateText = ''; + + privateAnnouncementsData.forEach((item) => { + if (item.type === 'discord_private') { + const privateOptions = item.options as DiscordPrivateOptions; + + if (privateOptions.roles) { + rolesArray.push(...privateOptions.roles); + } + + if (privateOptions.users) { + usersArray.push(...privateOptions.users); + } + + if (!templateText) { + templateText = item.template; + } + } + }); + + setPrivateMessage(true); + setSelectedRoles(rolesArray); + setSelectedUsers(usersArray); + setMessage(templateText); + } + }, [isEdit, privateAnnouncementsData]); + return (
@@ -111,7 +150,12 @@ function TcPrivateMessageContainer({ } + control={ + + } label={
@@ -184,6 +228,8 @@ function TcPrivateMessageContainer({ messageType !== MessageType.Both && messageType !== MessageType.RoleOnly } + isEdit={true} + privateSelectedRoles={selectedRoles} handleSelectedUsers={setSelectedRoles} /> @@ -201,6 +247,8 @@ function TcPrivateMessageContainer({ messageType !== MessageType.Both && messageType !== MessageType.UserOnly } + isEdit={true} + privateSelectedUsers={selectedUsers} handleSelectedUsers={setSelectedUsers} /> diff --git a/src/components/announcements/create/privateMessaageContainer/TcRolesAutoComplete.tsx b/src/components/announcements/create/privateMessaageContainer/TcRolesAutoComplete.tsx index d2f891c6..ff5eeb5a 100644 --- a/src/components/announcements/create/privateMessaageContainer/TcRolesAutoComplete.tsx +++ b/src/components/announcements/create/privateMessaageContainer/TcRolesAutoComplete.tsx @@ -7,11 +7,15 @@ import { useToken } from '../../../../context/TokenContext'; import { debounce, hexToRGBA, isDarkColor } from '../../../../helpers/helper'; interface ITcRolesAutoCompleteProps { + isEdit?: boolean; + privateSelectedRoles?: IRoles[]; isDisabled: boolean; handleSelectedUsers: (roles: IRoles[]) => void; } function TcRolesAutoComplete({ + isEdit = false, + privateSelectedRoles, isDisabled, handleSelectedUsers, }: ITcRolesAutoCompleteProps) { @@ -32,6 +36,7 @@ function TcRolesAutoComplete({ totalResults: 0, }); const [filteredRolesByName, setFilteredRolesByName] = useState(''); + const [isInitialized, setIsInitialized] = useState(false); const fetchDiscordRoles = async ( platformId: string, @@ -127,6 +132,17 @@ function TcRolesAutoComplete({ handleSelectedUsers(selectedRoles); }, [selectedRoles]); + useEffect(() => { + if (isEdit && !isInitialized) { + if (privateSelectedRoles !== undefined) { + setSelectedRoles(privateSelectedRoles); + } else { + setSelectedRoles([]); + } + setIsInitialized(true); + } + }, [privateSelectedRoles, isEdit, isInitialized]); + return ( void; } function TcUsersAutoComplete({ + isEdit = false, + privateSelectedUsers, isDisabled, handleSelectedUsers, }: ITcUsersAutoCompleteProps) { @@ -32,24 +36,25 @@ function TcUsersAutoComplete({ totalResults: 0, }); const [filteredRolesByName, setFilteredRolesByName] = useState(''); + const [isInitialized, setIsInitialized] = useState(false); const fetchDiscordUsers = async ( platformId: string, page?: number, limit?: number, - name?: string + ngu?: string ) => { try { const fetchedUsers = await retrievePlatformProperties({ platformId, - name: name, + ngu: ngu, property: 'guildMember', page: page, limit: limit, }); - if (name) { - setFilteredRolesByName(name); + if (ngu) { + setFilteredRolesByName(ngu); setFetchedUsers(fetchedUsers); } else { setFetchedUsers((prevData: { results: any }) => { @@ -127,6 +132,17 @@ function TcUsersAutoComplete({ handleSelectedUsers(selectedUsers); }, [selectedUsers]); + useEffect(() => { + if (isEdit && !isInitialized) { + if (privateSelectedUsers !== undefined) { + setSelectedUsers(privateSelectedUsers); + } else { + setSelectedUsers([]); + } + setIsInitialized(true); + } + }, [privateSelectedUsers, isEdit, isInitialized]); + return ( { + if (isEdit && publicAnnouncementsData) { + if ( + publicAnnouncementsData.type === 'discord_public' && + 'channels' in publicAnnouncementsData.options + ) { + const formattedChannels = publicAnnouncementsData.options.channels.map( + (channel) => ({ + id: channel.channelId, + label: channel.name, + }) + ); + setSelectedChannels(formattedChannels); + setMessage(publicAnnouncementsData.template); + } + } + }, [isEdit, publicAnnouncementsData]); + return (
diff --git a/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx index 52c19986..d8ddc3a0 100644 --- a/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx +++ b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx @@ -7,10 +7,14 @@ import moment from 'moment'; import TcDateTimePopover from './TcDateTimePopover'; export interface ITcScheduleAnnouncementProps { + isEdit?: boolean; + preSelectedTime?: string; handleSchaduledDate: ({ selectedTime }: { selectedTime: string }) => void; } function TcScheduleAnnouncement({ + isEdit = false, + preSelectedTime, handleSchaduledDate, }: ITcScheduleAnnouncementProps) { const [anchorEl, setAnchorEl] = useState(null); @@ -69,6 +73,15 @@ function TcScheduleAnnouncement({ handleSchaduledDate({ selectedTime: formattedUTCDate }); }, [selectedDate, selectedTime]); + useEffect(() => { + if (isEdit) { + const date = moment(preSelectedTime); + setSelectedDate(date.toDate()); + setSelectedTime(date.toDate()); + setDateTimeDisplay(date.format('D MMMM YYYY @ h A')); + } + }, [isEdit]); + return (
diff --git a/src/components/pages/pageIndex/HeatmapChart.tsx b/src/components/pages/pageIndex/HeatmapChart.tsx index d578ea17..448db7f9 100644 --- a/src/components/pages/pageIndex/HeatmapChart.tsx +++ b/src/components/pages/pageIndex/HeatmapChart.tsx @@ -58,6 +58,8 @@ const HeatmapChart = () => { const fetchData = async () => { setLoading(true); try { + console.log({ selectedSubChannels }); + if (platformId) { const data = await fetchHeatmapData( platformId, @@ -94,11 +96,15 @@ const HeatmapChart = () => { }; useEffect(() => { +<<<<<<< Updated upstream const initializeSelectedChannels = async () => { await fetchPlatformChannels(); }; initializeSelectedChannels(); +======= + fetchPlatformChannels(); +>>>>>>> Stashed changes }, []); const handleSelectedZone = (zone: string) => { @@ -164,7 +170,11 @@ const HeatmapChart = () => { } else { await refreshData(platformId); } +<<<<<<< Updated upstream setPlatformFetched(true); +======= + await fetchData(); +>>>>>>> Stashed changes } } catch (error) { } finally { diff --git a/src/pages/announcements/edit-announcements/index.tsx b/src/pages/announcements/edit-announcements/index.tsx index cd74aa84..22f159c1 100644 --- a/src/pages/announcements/edit-announcements/index.tsx +++ b/src/pages/announcements/edit-announcements/index.tsx @@ -1,19 +1,162 @@ -import React from 'react'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; import { defaultLayout } from '../../../layouts/defaultLayout'; import SEO from '../../../components/global/SEO'; -import router from 'next/router'; +import { useRouter } from 'next/router'; import TcPrivateMessaageContainer from '../../../components/announcements/create/privateMessaageContainer'; import TcPublicMessaageContainer from '../../../components/announcements/create/publicMessageContainer'; import TcScheduleAnnouncement from '../../../components/announcements/create/scheduleAnnouncement'; import TcSelectPlatform from '../../../components/announcements/create/selectPlatform'; import TcBoxContainer from '../../../components/shared/TcBox/TcBoxContainer'; import TcBreadcrumbs from '../../../components/shared/TcBreadcrumbs'; -import TcButton from '../../../components/shared/TcButton'; import TcConfirmSchaduledAnnouncementsDialog from '../../../components/announcements/TcConfirmSchaduledAnnouncementsDialog'; -import { FlattenedChannel } from '../../../components/announcements/create/publicMessageContainer/TcPublicMessageContainer'; +import useAppStore from '../../../store/useStore'; import { IRoles, IUser } from '../../../utils/interfaces'; +import { ChannelContext } from '../../../context/ChannelContext'; +import { useSnackbar } from '../../../context/SnackbarContext'; +import { useToken } from '../../../context/TokenContext'; +import { CreateAnnouncementsPayloadData } from '../create-new-announcements'; + +export interface DiscordChannel { + channelId: string; + name: string; +} + +interface DiscordUser { + discordId: string; + ngu: string; +} + +interface DiscordPublicOptions { + channels: DiscordChannel[]; +} + +export interface DiscordPrivateOptions { + roles?: IRoles[]; + users?: DiscordUser[]; +} + +type DiscordOptions = DiscordPublicOptions | DiscordPrivateOptions; + +export interface DiscordData { + platform: string; + template: string; + options: DiscordOptions; + type: 'discord_public' | 'discord_private'; +} + +export interface AnnouncementsDiscordResponseProps { + id: string; + scheduledAt: string; + draft: boolean; + data: DiscordData[]; + community: string; +} function Index() { + const { retrieveAnnouncementById, patchExistingAnnouncement } = useAppStore(); + + const router = useRouter(); + + const { community } = useToken(); + + const channelContext = useContext(ChannelContext); + const { refreshData } = channelContext; + + const { showMessage } = useSnackbar(); + + const [channels, setChannels] = useState([]); + const [roles, setRoles] = useState([]); + const [users, setUsers] = useState([]); + + const platformId = community?.platforms.find( + (platform) => platform.disconnectedAt === null + )?.id; + + const [publicAnnouncements, setPublicAnnouncements] = + useState(); + + const [privateAnnouncements, setPrivateAnnouncements] = + useState(); + + const [fetchedAnnouncements, setFetchedAnnouncements] = + useState(); + + const id = router.query.announcementsId as string; + + const [scheduledAt, setScheduledAt] = useState(); + + const publicSelectedAnnouncements = useMemo(() => { + return fetchedAnnouncements?.data.filter( + (item) => item.type === 'discord_public' + )[0]; + }, [fetchedAnnouncements]); + + const privateSelectedAnnouncements = useMemo(() => { + return fetchedAnnouncements?.data.filter( + (item) => item.type === 'discord_private' + ); + }, [fetchedAnnouncements]); + + const fetchPlatformChannels = async () => { + try { + if (platformId) { + let channelIds: string[] = []; + + if ( + publicSelectedAnnouncements?.type === 'discord_public' && + 'channels' in publicSelectedAnnouncements.options + ) { + channelIds = publicSelectedAnnouncements.options.channels.map( + (channel) => channel.channelId + ); + } + + await refreshData(platformId, 'channel', channelIds, undefined, false); + } + } catch (error) { + } finally { + } + }; + + useEffect(() => { + if (!id) return; + const fetchAnnouncement = async () => { + const data = await retrieveAnnouncementById(id); + setFetchedAnnouncements(data); + }; + + fetchAnnouncement(); + fetchPlatformChannels(); + }, [id]); + + const handleEditAnnouncements = async (isDrafted: boolean) => { + if (!community) return; + + const data = [publicAnnouncements]; + + if (privateAnnouncements && privateAnnouncements.length > 0) { + data.push(...privateAnnouncements); + } + + const announcementsPayload = { + draft: isDrafted, + scheduledAt: scheduledAt, + data: data, + }; + + try { + console.log(id, announcementsPayload); + + const data = await patchExistingAnnouncement(id, announcementsPayload); + if (data) { + showMessage('Announcement updated successfully', 'success'); + router.push('/announcements'); + } + } catch (error) { + showMessage('Failed to create announcement', 'error'); + } + }; + return ( <> @@ -30,64 +173,94 @@ function Index() {
{ + if (!platformId) return; + setChannels(selectedChannels); + setPublicAnnouncements({ + platformId: platformId, + template: message, + options: { + channelIds: selectedChannels.map( + (channel) => channel.id + ), + }, + }); }} /> { + if (!platformId) return; + + let rolePrivateAnnouncements; + let userPrivateAnnouncements; + + const commonData = { + platformId: platformId, + template: message, + }; + + if (selectedRoles && selectedRoles.length > 0) { + setRoles(selectedRoles); + + rolePrivateAnnouncements = { + ...commonData, + options: { + roleIds: selectedRoles.map((role) => + role.roleId.toString() + ), + }, + }; + } + + if (selectedUsers && selectedUsers.length > 0) { + setUsers(selectedUsers); + + userPrivateAnnouncements = { + ...commonData, + options: { + userIds: selectedUsers.map((user) => user.discordId), + }, + }; + } + + const announcements = []; + if (rolePrivateAnnouncements) + announcements.push(rolePrivateAnnouncements); + if (userPrivateAnnouncements) + announcements.push(userPrivateAnnouncements); + + setPrivateAnnouncements(announcements); }} /> { + setScheduledAt(selectedTime); }} />
-
- router.push('/announcements')} - variant="outlined" - sx={{ - maxWidth: { - xs: '100%', - sm: '8rem', - }, - }} +
+ handleEditAnnouncements(e)} /> -
- -
} diff --git a/src/pages/announcements/index.tsx b/src/pages/announcements/index.tsx index 2c84e671..3657be7e 100644 --- a/src/pages/announcements/index.tsx +++ b/src/pages/announcements/index.tsx @@ -104,8 +104,6 @@ function Index() { }; useEffect(() => { - console.log({ selectedDate }); - fetchData(selectedDate, selectedZone); }, [selectedZone, selectedDate, page]); diff --git a/src/store/slices/announcementsSlice.ts b/src/store/slices/announcementsSlice.ts index 5ef14cbf..1d2a06fb 100644 --- a/src/store/slices/announcementsSlice.ts +++ b/src/store/slices/announcementsSlice.ts @@ -10,7 +10,7 @@ const createAnnouncementsSlice: StateCreator = (set, get) => ({ page, limit, sortBy, - name, + ngu, timeZone, startDate, endDate, @@ -24,7 +24,7 @@ const createAnnouncementsSlice: StateCreator = (set, get) => ({ ...(timeZone ? { timeZone } : {}), ...(startDate ? { startDate } : {}), ...(endDate ? { endDate } : {}), - ...(name ? { name } : {}), + ...(ngu ? { name } : {}), }; const { data } = await axiosInstance.get( @@ -58,9 +58,17 @@ const createAnnouncementsSlice: StateCreator = (set, get) => ({ console.error('Failed to create announcements:', error); } }, - patchExistingAnnouncement: async (id: string) => { + patchExistingAnnouncement: async ( + id: string, + announcementPayload: CreateAnnouncementsPayload + ) => { try { - const { data } = await axiosInstance.post(`/announcements/${id}`); + console.log({ id }); + + const { data } = await axiosInstance.patch( + `/announcements/${id}`, + announcementPayload + ); return data; } catch (error) { console.error('Failed to patch announcements:', error); diff --git a/src/store/types/IAnnouncements.ts b/src/store/types/IAnnouncements.ts index e631ae48..37937eac 100644 --- a/src/store/types/IAnnouncements.ts +++ b/src/store/types/IAnnouncements.ts @@ -2,7 +2,7 @@ export interface IRetrieveAnnouncementsProps { page: number; limit: number; sortBy?: string; - name?: string; + ngu?: string; community: string; startDate?: string; endDate?: string; @@ -14,7 +14,7 @@ export default interface IAnnouncements { page, limit, sortBy, - name, + ngu, community, }: IRetrieveAnnouncementsProps) => void; } diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 543828b3..59165c1e 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -27,8 +27,8 @@ export interface IRoles { roleId: string; color: number | string; name: string; - deletedAt: string; - id: number | string; + deletedAt?: string; + id?: number | string; } export interface IUserProfile { @@ -174,9 +174,9 @@ export interface IDiscordModifiedCommunity export interface IUser { discordId: string; - discriminator: string; - globalName: string | null; + discriminator?: string; + globalName?: string | null; ngu: string; - nickname: string | null; - username: string; + nickname?: string | null; + username?: string; } From f353b41016b7963e3b77435671854bd04a0a9a23 Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 19 Jan 2024 04:06:49 +0300 Subject: [PATCH 25/30] complete announcement feature --- .../TcAnnouncementsAlert.spec.tsx | 29 ++ .../announcements/TcAnnouncementsAlert.tsx | 69 ++++ .../announcements/TcAnnouncementsTable.tsx | 296 +++++++++++++----- ...nfirmSchaduledAnnouncementsDialog.spec.tsx | 91 ++---- .../announcements/TcTimeZone.spec.tsx | 24 +- src/components/announcements/TcTimeZone.tsx | 2 +- .../TcPrivateMessaageContainer.spec.tsx | 43 --- ...iner.tsx => TcPrivateMessageContainer.tsx} | 0 .../TcRolesAutoComplete.tsx | 35 ++- .../TcUsersAutoComplete.tsx | 22 +- .../create/privateMessaageContainer/index.ts | 2 +- .../pages/pageIndex/HeatmapChart.tsx | 8 - .../TcTableContainer/TcTableRow.spec.tsx | 9 - .../create-new-announcements.tsx | 14 +- .../edit-announcements/index.tsx | 13 +- src/pages/announcements/index.tsx | 69 +++- src/pages/callback.tsx | 8 + src/store/slices/platformSlice.ts | 10 + src/store/types/IPlatform.ts | 11 + src/utils/enums.ts | 13 + src/utils/interfaces.ts | 22 ++ 21 files changed, 538 insertions(+), 252 deletions(-) create mode 100644 src/components/announcements/TcAnnouncementsAlert.spec.tsx create mode 100644 src/components/announcements/TcAnnouncementsAlert.tsx delete mode 100644 src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.spec.tsx rename src/components/announcements/create/privateMessaageContainer/{TcPrivateMessaageContainer.tsx => TcPrivateMessageContainer.tsx} (100%) diff --git a/src/components/announcements/TcAnnouncementsAlert.spec.tsx b/src/components/announcements/TcAnnouncementsAlert.spec.tsx new file mode 100644 index 00000000..2da01b59 --- /dev/null +++ b/src/components/announcements/TcAnnouncementsAlert.spec.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import TcAnnouncementsAlert from './TcAnnouncementsAlert'; + +const mockGrantWritePermissions = jest.fn(); + +jest.mock('../../store/useStore', () => () => ({ + grantWritePermissions: mockGrantWritePermissions, +})); + +jest.mock('../../context/TokenContext', () => ({ + useToken: () => ({ + community: { + platforms: [ + { id: '1', disconnectedAt: null, metadata: { id: '3123141414221' } }, + ], + }, + }), +})); + +describe('TcAnnouncementsAlert', () => { + it('renders correctly', () => { + render(); + expect( + screen.getByText(/Announcements needs write access at the server-level/i) + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/announcements/TcAnnouncementsAlert.tsx b/src/components/announcements/TcAnnouncementsAlert.tsx new file mode 100644 index 00000000..be5f9b7b --- /dev/null +++ b/src/components/announcements/TcAnnouncementsAlert.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import TcAlert from '../shared/TcAlert'; +import TcCollapse from '../shared/TcCollapse'; +import TcText from '../shared/TcText'; +import TcButton from '../shared/TcButton'; +import useAppStore from '../../store/useStore'; +import { useToken } from '../../context/TokenContext'; + +function TcAnnouncementsAlert() { + const { grantWritePermissions } = useAppStore(); + + const { community } = useToken(); + + const handleGrantAccess = () => { + const guildId = community?.platforms.find( + (platform) => platform.disconnectedAt === null + )?.metadata.id; + + if (guildId) + grantWritePermissions({ + platformType: 'discord', + moduleType: 'Announcement', + id: guildId, + }); + }; + return ( + + +
+ + +
+
+
+ ); +} + +export default TcAnnouncementsAlert; diff --git a/src/components/announcements/TcAnnouncementsTable.tsx b/src/components/announcements/TcAnnouncementsTable.tsx index 122d3ffa..ace0bfe0 100644 --- a/src/components/announcements/TcAnnouncementsTable.tsx +++ b/src/components/announcements/TcAnnouncementsTable.tsx @@ -8,6 +8,7 @@ import { IconButton, MenuItem, Menu, + Chip, } from '@mui/material'; import { BsThreeDotsVertical } from 'react-icons/bs'; import Router from 'next/router'; @@ -18,6 +19,8 @@ import TcText from '../shared/TcText'; import useAppStore from '../../store/useStore'; import { useSnackbar } from '../../context/SnackbarContext'; import { MdModeEdit, MdDelete } from 'react-icons/md'; +import Loading from '../global/Loading'; +import { truncateCenter } from '../../helpers/helper'; interface Channel { channelId: string; @@ -38,6 +41,7 @@ interface Role { interface AnnouncementData { platform: string; template: string; + type: 'discord_public' | 'discord_private'; options: { channels: Channel[]; users?: User[]; @@ -56,11 +60,13 @@ interface Announcement { interface AnnouncementsTableProps { announcements: Announcement[]; + isLoading: boolean; handleRefreshList: () => void; } function TcAnnouncementsTable({ announcements, + isLoading, handleRefreshList, }: AnnouncementsTableProps) { const { deleteAnnouncements } = useAppStore(); @@ -96,8 +102,7 @@ function TcAnnouncementsTable({ const handleDeleteAnnouncements = async (id: string) => { try { - const data = await deleteAnnouncements(id); - + await deleteAnnouncements(id); handleRefreshList(); showMessage('Scheduled announcement removed successfully.', 'success'); } catch (error) { @@ -109,6 +114,212 @@ function TcAnnouncementsTable({ } }; + const getAnnouncementTypeLabel = (type: string) => { + if (type === 'discord_public') { + return 'Public'; + } else if (type === 'discord_private') { + return 'Private'; + } + return 'Unknown'; + }; + + const renderTableBody = () => { + if (isLoading) { + return ( + + + + + + ); + } + + return announcements.map((announcement, index) => ( + + +
+ + + {truncateCenter(announcement.data[0].template, 20)} + + } + variant="subtitle2" + /> + + {announcement.data + .reduce((unique: string[], item: AnnouncementData) => { + const itemType = item.type; + if (!unique.includes(itemType)) { + unique.push(itemType); + } + return unique; + }, [] as string[]) + .map((type, index) => ( + + + {getAnnouncementTypeLabel(type)} +
+ } + size="small" + sx={{ + borderRadius: '4px', + borderColor: '#D1D1D1', + backgroundColor: 'white', + color: 'black', + }} + /> + ))} + +
+ + +
+ { + const channels = item.options.channels; + if (channels && channels.length > 0) { + const displayedChannels = channels + .slice(0, 2) + .map((channel) => `#${channel.name}`) + .join(', '); + const moreChannelsIndicator = + channels.length > 2 ? '...' : ''; + return dataIndex > 0 + ? `, ${displayedChannels}${moreChannelsIndicator}` + : `${displayedChannels}${moreChannelsIndicator}`; + } + return ''; + }) + .filter((text) => text !== '') + .join('')} + variant="subtitle2" + /> +
+
+ +
+ { + const users = item.options.users; + if (users && users.length > 0) { + const displayedUsers = users + .slice(0, 2) + .map((user) => `@${user.ngu}`) + .join(', '); + const moreUsersIndicator = users.length > 2 ? '...' : ''; + return `${displayedUsers}${moreUsersIndicator}`; + } + return ''; + }) + .filter((text) => text !== '') + .join(', ')} + variant="subtitle2" + /> +
+
+ +
+ { + const roles = item.options.roles; + if (roles && roles.length > 0) { + const displayedRoles = roles + .slice(0, 2) + .map((role) => role.name) + .join(', '); + const moreRolesIndicator = roles.length > 2 ? '...' : ''; + return `${displayedRoles}${moreRolesIndicator}`; + } + return ''; + }) + .filter((text) => text !== '') + .join(', ')} + variant="subtitle2" + /> +
+
+ +
+ +
+
+ + handleClick(event, announcement.id)} + > + + + + handleEdit(announcement.id)}> + + Edit + + + + Delete + + + + + )); + }; + return ( <>
@@ -121,6 +332,7 @@ function TcAnnouncementsTable({ width: '20%', borderBottom: 'none', whiteSpace: 'nowrap', + padding: '0 1rem', }} align="left" > @@ -133,6 +345,7 @@ function TcAnnouncementsTable({ width: '20%', borderBottom: 'none', whiteSpace: 'nowrap', + padding: '0 1rem', }} align="left" > @@ -145,6 +358,7 @@ function TcAnnouncementsTable({ width: '20%', borderBottom: 'none', whiteSpace: 'nowrap', + padding: '0 1rem', }} align="left" > @@ -157,6 +371,7 @@ function TcAnnouncementsTable({ width: '20%', borderBottom: 'none', whiteSpace: 'nowrap', + padding: '0 1rem', }} align="left" > @@ -169,6 +384,7 @@ function TcAnnouncementsTable({ width: '20%', borderBottom: 'none', whiteSpace: 'nowrap', + padding: '0 1rem', }} align="left" > @@ -181,85 +397,13 @@ function TcAnnouncementsTable({ width: '20%', borderBottom: 'none', whiteSpace: 'nowrap', + padding: '0 1rem', }} align="right" > - - {announcements.map((announcement, index) => ( - - - {announcement.data[0].template} - - - {announcement.data.map( - (item) => - item.options.channels && - item.options.channels - .map((channel) => `#${channel.name}`) - .join(', ') - )} - - - {announcement.data.map((item) => - item.options.users - ? item.options.users - .map((user) => `@${user.ngu}`) - .join(', ') - : '' - )} - - - {announcement.data.map((item) => - item.options.roles - ? item.options.roles.map((role) => role.name).join(', ') - : '' - )} - - - {new Date(announcement.scheduledAt).toLocaleString()} - - - handleClick(event, announcement.id)} - > - - - - handleEdit(announcement.id)}> - - Edit - - - - Delete - - - - - ))} - + {renderTableBody()}
{ - const defaultProps = { - buttonLabel: 'Test Button', - schaduledDate: 'July 12 at 13pm (CET)', - }; +const mockHandleCreateAnnouncements = jest.fn(); - it('renders without crashing', () => { - render( - - ); - expect(screen.getByText('Test Button')).toBeInTheDocument(); - }); +const defaultProps = { + buttonLabel: 'Schedule Announcement', + selectedChannels: [{ id: '1', label: 'General' }], + schaduledDate: '2024-01-20T12:00:00', + isDisabled: false, + handleCreateAnnouncements: mockHandleCreateAnnouncements, +}; - it('toggles dialog visibility on button click', async () => { - render( - - ); - const button = screen.getByText('Test Button'); - fireEvent.click(button); +test('renders the dialog with button and calls handleCreateAnnouncements when confirmed', () => { + const { getByText, getByTestId } = render( + + ); - await waitFor(() => { - expect(screen.getByText('Confirm Schedule')).toBeInTheDocument(); - }); + const button = getByText('Schedule Announcement'); + expect(button).toBeInTheDocument(); - const closeButton = screen.getByTestId('close-icon'); - fireEvent.click(closeButton); + fireEvent.click(button); - await waitFor(() => { - expect(screen.queryByText('Confirm Schedule')).not.toBeInTheDocument(); - }); - }); + const dialogTitle = getByText('Confirm Schedule'); + expect(dialogTitle).toBeInTheDocument(); - it('displays the correct dialog content', () => { - render( - - ); - fireEvent.click(screen.getByText('Test Button')); - expect( - screen.getByText('Discord announcements scheduled for:') - ).toBeInTheDocument(); - expect(screen.getByText('Public Message to:')).toBeInTheDocument(); - expect( - screen.getByText('Private Message to these user(s):') - ).toBeInTheDocument(); - expect( - screen.getByText('Private Message to these role(s):') - ).toBeInTheDocument(); - }); + const confirmButton = getByText('Confirm'); + expect(confirmButton).toBeInTheDocument(); + fireEvent.click(confirmButton); - it('closes the dialog when the close icon is clicked', async () => { - render( - - ); - - fireEvent.click(screen.getByText('Test Button')); - - fireEvent.click(screen.getByTestId('close-icon')); - - await waitFor(() => { - expect(screen.queryByText('Confirm Schedule')).not.toBeInTheDocument(); - }); - }); + expect(mockHandleCreateAnnouncements).toHaveBeenCalledWith(false); }); diff --git a/src/components/announcements/TcTimeZone.spec.tsx b/src/components/announcements/TcTimeZone.spec.tsx index b30eaca2..351de2bc 100644 --- a/src/components/announcements/TcTimeZone.spec.tsx +++ b/src/components/announcements/TcTimeZone.spec.tsx @@ -1,17 +1,17 @@ import React from 'react'; -import { render, fireEvent, screen } from '@testing-library/react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; import TcTimeZone from './TcTimeZone'; -describe('TcTimeZone', () => { - test('renders TcTimeZone component', () => { - render(); - expect(screen.getByRole('button')).toBeInTheDocument(); - }); +test('should handle zone selection', async () => { + const handleZoneFunction = jest.fn(); - test('opens popover on button click', () => { - render(); - const button = screen.getByRole('button'); - fireEvent.click(button); - expect(screen.getByText('Search timezone')).toBeInTheDocument(); - }); + const { getByTestId, getByLabelText } = render( + + ); + + const globeIcon = getByTestId('globe-icon'); + fireEvent.click(globeIcon); + + const searchInput = getByLabelText('Search timezone'); + expect(searchInput).toBeInTheDocument(); }); diff --git a/src/components/announcements/TcTimeZone.tsx b/src/components/announcements/TcTimeZone.tsx index 180f7af8..b9fdc857 100644 --- a/src/components/announcements/TcTimeZone.tsx +++ b/src/components/announcements/TcTimeZone.tsx @@ -62,7 +62,7 @@ function TcTimeZone({ handleZone }: ITcTimeZoneProps) { sx={{ height: '2.4rem', }} - startIcon={} + startIcon={} aria-describedby={id} onClick={handleClick} /> diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.spec.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.spec.tsx deleted file mode 100644 index e4827f8d..00000000 --- a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.spec.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { render, fireEvent, screen } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import TcPrivateMessageContainer from './TcPrivateMessaageContainer'; - -describe('TcPrivateMessageContainer Tests', () => { - test('renders the component without crashing', () => { - render(); - expect(screen.getByText('Private Message (optional)')).toBeInTheDocument(); - }); - - test('initial states are set correctly', () => { - render(); - }); - - test('toggles private message switch', () => { - render(); - const switchControl = screen.getByRole('checkbox'); - fireEvent.click(switchControl); - }); - - test('message type buttons respond to clicks', () => { - render(); - }); - - test('allows the user to enter a message', async () => { - render(); - - const privateMessageToggle = screen.getByRole('checkbox'); - fireEvent.click(privateMessageToggle); - - const messageInput = (await screen.findByPlaceholderText( - 'Write your message here' - )) as HTMLInputElement; - fireEvent.change(messageInput, { target: { value: 'Test Message' } }); - - expect(messageInput.value).toBe('Test Message'); - }); - - test('handles channel and username selection based on message type', () => { - render(); - }); -}); diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer.tsx similarity index 100% rename from src/components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer.tsx rename to src/components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer.tsx diff --git a/src/components/announcements/create/privateMessaageContainer/TcRolesAutoComplete.tsx b/src/components/announcements/create/privateMessaageContainer/TcRolesAutoComplete.tsx index ff5eeb5a..39a0496d 100644 --- a/src/components/announcements/create/privateMessaageContainer/TcRolesAutoComplete.tsx +++ b/src/components/announcements/create/privateMessaageContainer/TcRolesAutoComplete.tsx @@ -158,23 +158,30 @@ function TcRolesAutoComplete({ value.map((option, index) => ( + + {option.name} +
+ } size="small" sx={{ borderRadius: '4px', - borderColor: hexToRGBA( - option.color !== 0 - ? `#${option.color.toString(16).padStart(6, '0')}` - : '#96A5A6', - 1 - ), - backgroundColor: hexToRGBA( - option.color !== 0 - ? `#${option.color.toString(16).padStart(6, '0')}` - : '#96A5A6', - 0.8 - ), - color: isDarkColor(option.color) ? 'white' : 'black', + borderColor: '#D1D1D1', + backgroundColor: 'white', + color: 'black', }} {...getTagProps({ index })} /> diff --git a/src/components/announcements/create/privateMessaageContainer/TcUsersAutoComplete.tsx b/src/components/announcements/create/privateMessaageContainer/TcUsersAutoComplete.tsx index 178f98ae..0f66abbb 100644 --- a/src/components/announcements/create/privateMessaageContainer/TcUsersAutoComplete.tsx +++ b/src/components/announcements/create/privateMessaageContainer/TcUsersAutoComplete.tsx @@ -158,13 +158,27 @@ function TcUsersAutoComplete({ value.map((option, index) => ( + + {option.ngu} +
+ } size="small" sx={{ borderRadius: '4px', - borderColor: '#96A5A6', - backgroundColor: '#96A5A6', - color: 'white', + borderColor: '#D1D1D1', + backgroundColor: 'white', + color: 'black', }} {...getTagProps({ index })} /> diff --git a/src/components/announcements/create/privateMessaageContainer/index.ts b/src/components/announcements/create/privateMessaageContainer/index.ts index 800fd209..cfd2afd6 100644 --- a/src/components/announcements/create/privateMessaageContainer/index.ts +++ b/src/components/announcements/create/privateMessaageContainer/index.ts @@ -1,3 +1,3 @@ -import { default as TcPrivateMessaageContainer } from './TcPrivateMessaageContainer'; +import { default as TcPrivateMessaageContainer } from './TcPrivateMessageContainer'; export default TcPrivateMessaageContainer; diff --git a/src/components/pages/pageIndex/HeatmapChart.tsx b/src/components/pages/pageIndex/HeatmapChart.tsx index 448db7f9..526075d2 100644 --- a/src/components/pages/pageIndex/HeatmapChart.tsx +++ b/src/components/pages/pageIndex/HeatmapChart.tsx @@ -96,15 +96,11 @@ const HeatmapChart = () => { }; useEffect(() => { -<<<<<<< Updated upstream const initializeSelectedChannels = async () => { await fetchPlatformChannels(); }; initializeSelectedChannels(); -======= - fetchPlatformChannels(); ->>>>>>> Stashed changes }, []); const handleSelectedZone = (zone: string) => { @@ -170,11 +166,7 @@ const HeatmapChart = () => { } else { await refreshData(platformId); } -<<<<<<< Updated upstream setPlatformFetched(true); -======= - await fetchData(); ->>>>>>> Stashed changes } } catch (error) { } finally { diff --git a/src/components/shared/TcTableContainer/TcTableRow.spec.tsx b/src/components/shared/TcTableContainer/TcTableRow.spec.tsx index 857840cc..cdedc478 100644 --- a/src/components/shared/TcTableContainer/TcTableRow.spec.tsx +++ b/src/components/shared/TcTableContainer/TcTableRow.spec.tsx @@ -1,15 +1,6 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import TcTableRow from './TcTableRow'; -import TcTableCell from './TcTableCell'; - -describe('TcTableCell', () => { - it('renders correctly with children', () => { - render(Sample Content); - const cellContent = screen.getByText('Sample Content'); - expect(cellContent).toBeInTheDocument(); - }); -}); describe('TcTableRow', () => { it('renders correctly with row data', () => { diff --git a/src/pages/announcements/create-new-announcements.tsx b/src/pages/announcements/create-new-announcements.tsx index 60aa02cc..d086fa7f 100644 --- a/src/pages/announcements/create-new-announcements.tsx +++ b/src/pages/announcements/create-new-announcements.tsx @@ -3,7 +3,7 @@ import { defaultLayout } from '../../layouts/defaultLayout'; import SEO from '../../components/global/SEO'; import TcBoxContainer from '../../components/shared/TcBox/TcBoxContainer'; import TcPublicMessageContainer from '../../components/announcements/create/publicMessageContainer/TcPublicMessageContainer'; -import TcPrivateMessageContainer from '../../components/announcements/create/privateMessaageContainer/TcPrivateMessaageContainer'; +import TcPrivateMessageContainer from '../../components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer'; import TcButton from '../../components/shared/TcButton'; import TcScheduleAnnouncement from '../../components/announcements/create/scheduleAnnouncement/'; import TcSelectPlatform from '../../components/announcements/create/selectPlatform'; @@ -15,6 +15,7 @@ import { ChannelContext } from '../../context/ChannelContext'; import { IRoles, IUser } from '../../utils/interfaces'; import { useSnackbar } from '../../context/SnackbarContext'; import { useRouter } from 'next/router'; +import SimpleBackdrop from '../../components/global/LoadingBackdrop'; export type CreateAnnouncementsPayloadDataOptions = | { channelIds: string[]; userIds?: string[]; roleIds?: string[] } @@ -48,6 +49,7 @@ function CreateNewAnnouncements() { const [channels, setChannels] = useState([]); const [roles, setRoles] = useState([]); const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(false); const platformId = community?.platforms.find( (platform) => platform.disconnectedAt === null @@ -62,13 +64,16 @@ function CreateNewAnnouncements() { const [scheduledAt, setScheduledAt] = useState(); const fetchPlatformChannels = async () => { + setLoading(true); try { if (platformId) { await retrievePlatformById(platformId); await refreshData(platformId, 'channel', undefined, undefined, false); } + setLoading(false); } catch (error) { } finally { + setLoading(false); } }; @@ -97,6 +102,7 @@ function CreateNewAnnouncements() { }; try { + setLoading(true); const data = await createNewAnnouncements(announcementsPayload); if (data) { showMessage('Announcement created successfully', 'success'); @@ -104,9 +110,15 @@ function CreateNewAnnouncements() { } } catch (error) { showMessage('Failed to create announcement', 'error'); + } finally { + setLoading(false); } }; + if (loading) { + return ; + } + return ( <> diff --git a/src/pages/announcements/edit-announcements/index.tsx b/src/pages/announcements/edit-announcements/index.tsx index 22f159c1..0f9596c8 100644 --- a/src/pages/announcements/edit-announcements/index.tsx +++ b/src/pages/announcements/edit-announcements/index.tsx @@ -15,6 +15,7 @@ import { ChannelContext } from '../../../context/ChannelContext'; import { useSnackbar } from '../../../context/SnackbarContext'; import { useToken } from '../../../context/TokenContext'; import { CreateAnnouncementsPayloadData } from '../create-new-announcements'; +import SimpleBackdrop from '../../../components/global/LoadingBackdrop'; export interface DiscordChannel { channelId: string; @@ -67,6 +68,7 @@ function Index() { const [channels, setChannels] = useState([]); const [roles, setRoles] = useState([]); const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(false); const platformId = community?.platforms.find( (platform) => platform.disconnectedAt === null @@ -99,6 +101,7 @@ function Index() { const fetchPlatformChannels = async () => { try { + setLoading(true); if (platformId) { let channelIds: string[] = []; @@ -115,6 +118,7 @@ function Index() { } } catch (error) { } finally { + setLoading(false); } }; @@ -145,8 +149,7 @@ function Index() { }; try { - console.log(id, announcementsPayload); - + setLoading(true); const data = await patchExistingAnnouncement(id, announcementsPayload); if (data) { showMessage('Announcement updated successfully', 'success'); @@ -154,9 +157,15 @@ function Index() { } } catch (error) { showMessage('Failed to create announcement', 'error'); + } finally { + setLoading(false); } }; + if (loading) { + return ; + } + return ( <> diff --git a/src/pages/announcements/index.tsx b/src/pages/announcements/index.tsx index 3657be7e..2b8fc85a 100644 --- a/src/pages/announcements/index.tsx +++ b/src/pages/announcements/index.tsx @@ -12,25 +12,66 @@ import moment from 'moment'; import { MdCalendarMonth } from 'react-icons/md'; import useAppStore from '../../store/useStore'; import { StorageService } from '../../services/StorageService'; -import { FetchedData, IDiscordModifiedCommunity } from '../../utils/interfaces'; +import { + FetchedData, + IDiscordModifiedCommunity, + IPlatformProps, +} from '../../utils/interfaces'; import TcAnnouncementsTable from '../../components/announcements/TcAnnouncementsTable'; import TcDatePickerPopover from '../../components/shared/TcDatePickerPopover'; +import TcAnnouncementsAlert from '../../components/announcements/TcAnnouncementsAlert'; +import { useToken } from '../../context/TokenContext'; function Index() { - const { retrieveAnnouncements } = useAppStore(); + const { retrieveAnnouncements, retrievePlatformById } = useAppStore(); + const { community } = useToken(); + + const [loading, setLoading] = useState(false); const communityId = StorageService.readLocalStorage('community')?.id; const [anchorEl, setAnchorEl] = useState(null); - const [activeTab, setActiveTab] = useState(0); const [selectedDate, setSelectedDate] = useState(null); const [selectedZone, setSelectedZone] = useState(moment.tz.guess()); const [dateTimeDisplay, setDateTimeDisplay] = useState('Filter Date'); - const [loading, setLoading] = useState(false); const [page, setPage] = useState(1); + const platformId = community?.platforms.find( + (platform) => platform.disconnectedAt === null + )?.id; + + const [announcementsPermissions, setAnnouncementsPermissions] = + useState(true); + + const fetchPlatform = async () => { + if (platformId) { + try { + setLoading(true); + const data: IPlatformProps = await retrievePlatformById(platformId); + const { metadata } = data; + + if (metadata) { + const announcements = metadata.permissions.Announcement; + const allPermissionsTrue = Object.values(announcements).every( + (value) => value === true + ); + + setAnnouncementsPermissions(allPermissionsTrue); + } + setLoading(false); + } catch (error) { + } finally { + setLoading(false); + } + } + }; + + useEffect(() => { + fetchPlatform(); + }, [platformId]); + const [fetchedAnnouncements, setFetchedAnnouncements] = useState( { limit: 8, @@ -114,6 +155,7 @@ function Index() { return ( <> + {!announcementsPermissions && }
{fetchedAnnouncements.results.length > 0 ? ( - +
+ +
) : (
= (set, get) => ({ return data; } catch (error) {} }, + grantWritePermissions: ({ + platformType, + moduleType, + id, + }: IGrantWritePermissionsProps) => { + location.replace( + `${BASE_URL}/platforms/request-access/${platformType}/${moduleType}/${id}` + ); + }, }); export default createPlatfromSlice; diff --git a/src/store/types/IPlatform.ts b/src/store/types/IPlatform.ts index c03294de..f88e42ca 100644 --- a/src/store/types/IPlatform.ts +++ b/src/store/types/IPlatform.ts @@ -31,6 +31,12 @@ export interface IPatchPlatformInput { }; } +export interface IGrantWritePermissionsProps { + platformType: 'discord' | 'telegram'; + moduleType: 'Announcements'; + id: string; +} + export default interface IPlatfrom { connectedPlatforms: any[]; connectNewPlatform: (platfromType: string) => void; @@ -56,4 +62,9 @@ export default interface IPlatfrom { page, limit, }: IRetrivePlatformRolesOrChannels) => void; + grantWritePermissions: ({ + platformType, + moduleType, + id, + }: IGrantWritePermissionsProps) => void; } diff --git a/src/utils/enums.ts b/src/utils/enums.ts index 957793c5..59908f05 100644 --- a/src/utils/enums.ts +++ b/src/utils/enums.ts @@ -14,4 +14,17 @@ export enum StatusCode { DISCORD_AUTHORIZATION_FAILURE_FROM_SETTINGS = '1005', TWITTER_AUTHORIZATION_SUCCESSFUL = '1006', TWITTER_AUTHORIZATION_FAILURE = '1007', + ANNOUNCEMENTS_PERMISSION_SUCCESS = '1008', + ANNOUNCEMENTS_PERMISSION_FAILURE = '1009', +} + +export enum Permission { + AttachFiles = 'Attach Files', + CreatePrivateThreads = 'Create Private Threads', + CreatePublicThreads = 'Create Public Threads', + EmbedLinks = 'Embed Links', + MentionEveryone = 'Mention Everyone', + SendMessages = 'Send Messages', + SendMessagesInThreads = 'Send Messages In Threads', + ViewChannel = 'View Channel', } diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 59165c1e..f34013e6 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -148,6 +148,27 @@ export interface IPlatformProps { metadata: metaData; } +export interface UserPermissions { + AttachFiles: boolean; + CreatePrivateThreads: boolean; + CreatePublicThreads: boolean; + EmbedLinks: boolean; + MentionEveryone: boolean; + SendMessages: boolean; + SendMessagesInThreads: boolean; + ViewChannel: boolean; +} + +export interface ReadData { + ViewChannel: boolean; + ReadMessageHistory: boolean; +} + +export interface Permissions { + permissions: UserPermissions; + ReadData: ReadData; +} + export interface ICommunityDiscordPlatfromProps { id: string; name: string; @@ -157,6 +178,7 @@ export interface ICommunityDiscordPlatfromProps { name: string; selectedChannels?: string[]; period?: string; + permissions: Permissions; analyzerStartedAt?: string; isInProgress?: boolean; }; From e3f8e16a6600cb4fc1db2048915b86eeabcf20db Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 19 Jan 2024 04:12:20 +0300 Subject: [PATCH 26/30] fix style --- .../selectCommunity/TcSelectCommunity.tsx | 2 ++ .../communitySettings/platform/TcPlatform.tsx | 1 + .../platform/TcPlatformChannelDialog.tsx | 1 + src/components/global/CustomTab.tsx | 19 ++++++++++++ src/pages/centric/create-new-community.tsx | 1 + src/pages/centric/index.tsx | 1 + src/pages/centric/tac.tsx | 1 + src/utils/theme.ts | 30 +------------------ 8 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/components/centric/selectCommunity/TcSelectCommunity.tsx b/src/components/centric/selectCommunity/TcSelectCommunity.tsx index dd3619ff..d4b304db 100644 --- a/src/components/centric/selectCommunity/TcSelectCommunity.tsx +++ b/src/components/centric/selectCommunity/TcSelectCommunity.tsx @@ -111,6 +111,7 @@ function TcSelectCommunity() { text="Continue" className="secondary" variant="contained" + sx={{ width: '15rem', padding: '0.5rem' }} disabled={!activeCommunity} onClick={handleSelectedCommunity} /> @@ -124,6 +125,7 @@ function TcSelectCommunity() { } text="Create" + sx={{ width: '15rem', padding: '0.5rem' }} variant="outlined" onClick={() => router.push('/centric/create-new-community')} /> diff --git a/src/components/communitySettings/platform/TcPlatform.tsx b/src/components/communitySettings/platform/TcPlatform.tsx index 7dc798a1..86aa358b 100644 --- a/src/components/communitySettings/platform/TcPlatform.tsx +++ b/src/components/communitySettings/platform/TcPlatform.tsx @@ -165,6 +165,7 @@ function TcPlatform({ platformName = 'Discord' }: TcPlatformProps) {
diff --git a/src/components/communitySettings/platform/TcPlatformChannelDialog.tsx b/src/components/communitySettings/platform/TcPlatformChannelDialog.tsx index edf07c3b..e705a215 100644 --- a/src/components/communitySettings/platform/TcPlatformChannelDialog.tsx +++ b/src/components/communitySettings/platform/TcPlatformChannelDialog.tsx @@ -69,6 +69,7 @@ function TcPlatformChannelDialog() { setOpenDialog(false)} />
diff --git a/src/components/global/CustomTab.tsx b/src/components/global/CustomTab.tsx index 3381e444..99af64eb 100644 --- a/src/components/global/CustomTab.tsx +++ b/src/components/global/CustomTab.tsx @@ -48,6 +48,25 @@ function CustomTab({ width: '50%', padding: '0', }, + textTransform: 'none', + borderRadius: '10px 10px 0 0', + padding: '8px 24px', + gap: '10px', + borderBottom: 'none', + '&.Mui-selected': { + background: '#804EE1', + color: 'white', + border: 0, + borderBottom: 'none', + }, + '&$selected': { + borderBottom: 'none', + }, + '&:not(.Mui-selected)': { + backgroundColor: '#EDEDED', + color: '#222222', + }, + selected: {}, }} /> ))} diff --git a/src/pages/centric/create-new-community.tsx b/src/pages/centric/create-new-community.tsx index d32928c8..cf13cdaa 100644 --- a/src/pages/centric/create-new-community.tsx +++ b/src/pages/centric/create-new-community.tsx @@ -98,6 +98,7 @@ function CreateNewCommunity() { handleCreateNewCommunitie()} diff --git a/src/pages/centric/index.tsx b/src/pages/centric/index.tsx index e6b655f5..c6ea8714 100644 --- a/src/pages/centric/index.tsx +++ b/src/pages/centric/index.tsx @@ -23,6 +23,7 @@ function Index() {
discordAuthorization()} /> diff --git a/src/pages/centric/tac.tsx b/src/pages/centric/tac.tsx index 65cfbdbc..cfdff9dd 100644 --- a/src/pages/centric/tac.tsx +++ b/src/pages/centric/tac.tsx @@ -96,6 +96,7 @@ function Tac() { handleAcceptTerms()} /> diff --git a/src/utils/theme.ts b/src/utils/theme.ts index 299635df..851c6743 100644 --- a/src/utils/theme.ts +++ b/src/utils/theme.ts @@ -14,10 +14,6 @@ export const theme = createTheme({ components: { MuiButton: { styleOverrides: { - // sizeMedium: { - // width: '15rem', - // padding: '0.5rem', - // }, root: { textTransform: 'none', borderRadius: '4px', @@ -109,31 +105,7 @@ export const theme = createTheme({ }, }, MuiTab: { - styleOverrides: { - // root: { - // textTransform: 'none', - // borderRadius: '10px 10px 0 0', - // padding: '8px 24px', - // width: '214px', - // height: '40px', - // gap: '10px', - // borderBottom: 'none', - // '&.Mui-selected': { - // background: '#804EE1', - // color: 'white', - // border: 0, - // borderBottom: 'none', - // }, - // '&$selected': { - // borderBottom: 'none', - // }, - // '&:not(.Mui-selected)': { - // backgroundColor: '#EDEDED', - // color: '#222222', - // }, - // selected: {}, - // }, - }, + styleOverrides: {}, }, }, }); From 7e265095f27489127cbdb921b52e16a173343256 Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 19 Jan 2024 11:17:12 +0300 Subject: [PATCH 27/30] fix issue on fetch data and styles --- .../announcements/TcAnnouncementsTable.tsx | 2 +- .../TcConfirmSchaduledAnnouncementsDialog.tsx | 11 ++++++ .../TcPrivateMessageContainer.tsx | 4 +-- .../TcPrivateMessagePreviewDialog.tsx | 4 +++ src/components/shared/TcBreadcrumbs.tsx | 2 -- .../edit-announcements/index.tsx | 6 +++- src/pages/announcements/index.tsx | 35 +++++++++---------- 7 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/components/announcements/TcAnnouncementsTable.tsx b/src/components/announcements/TcAnnouncementsTable.tsx index ace0bfe0..72a6e357 100644 --- a/src/components/announcements/TcAnnouncementsTable.tsx +++ b/src/components/announcements/TcAnnouncementsTable.tsx @@ -131,7 +131,7 @@ function TcAnnouncementsTable({ colSpan={6} style={{ textAlign: 'center' }} sx={{ borderBottom: 'none' }} - className="min-h-[70vh] pt-[14rem]" + className="min-h-[70vh] pt-[20dvh]" data-testid="loading-indicator" > diff --git a/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx index 7e7c9cec..027f9bec 100644 --- a/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx +++ b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx @@ -46,6 +46,17 @@ function TcConfirmSchaduledAnnouncementsDialog({ <> setConfirmSchadulerDialog(true)} diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer.tsx index e97ddcd6..7db12e65 100644 --- a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer.tsx +++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer.tsx @@ -129,11 +129,11 @@ function TcPrivateMessageContainer({ if (!templateText) { templateText = item.template; + setPrivateMessage(true); } } }); - setPrivateMessage(true); setSelectedRoles(rolesArray); setSelectedUsers(usersArray); setMessage(templateText); @@ -213,7 +213,7 @@ function TcPrivateMessageContainer({ className="text-gray-400" />
-
+
channel.channelId ); } + console.log({ channelIds }); await refreshData(platformId, 'channel', channelIds, undefined, false); } @@ -130,9 +131,12 @@ function Index() { }; fetchAnnouncement(); - fetchPlatformChannels(); }, [id]); + useEffect(() => { + fetchPlatformChannels(); + }, [fetchedAnnouncements]); + const handleEditAnnouncements = async (isDrafted: boolean) => { if (!community) return; diff --git a/src/pages/announcements/index.tsx b/src/pages/announcements/index.tsx index 2b8fc85a..12ec5276 100644 --- a/src/pages/announcements/index.tsx +++ b/src/pages/announcements/index.tsx @@ -159,8 +159,8 @@ function Index() {
-
+
+
)}
- - {fetchedAnnouncements.totalResults > 8 ? ( -
- -
- ) : ( - '' - )} +
+ {fetchedAnnouncements.totalResults > 8 && ( +
+ +
+ )} +
} /> From 8e2f91aa571f6109df1fbc846dc326921ff5350e Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 19 Jan 2024 13:12:46 +0300 Subject: [PATCH 28/30] fix styles on table and filter issue --- .../announcements/TcAnnouncementsTable.tsx | 221 +++++++++++------- src/pages/announcements/index.tsx | 10 +- 2 files changed, 144 insertions(+), 87 deletions(-) diff --git a/src/components/announcements/TcAnnouncementsTable.tsx b/src/components/announcements/TcAnnouncementsTable.tsx index 72a6e357..cce6de93 100644 --- a/src/components/announcements/TcAnnouncementsTable.tsx +++ b/src/components/announcements/TcAnnouncementsTable.tsx @@ -123,33 +123,18 @@ function TcAnnouncementsTable({ return 'Unknown'; }; - const renderTableBody = () => { - if (isLoading) { - return ( - - - - - - ); - } - - return announcements.map((announcement, index) => ( - - + const renderTableCell = ( + announcement: { + draft: any; + data: any[]; + scheduledAt: string | number | Date; + id: string | null; + }, + cellType: any + ) => { + switch (cellType) { + case 'title': + return (
( + .map((type: string, index: React.Key | null | undefined) => (
-
- + ); + case 'channels': + return (
{ - const channels = item.options.channels; - if (channels && channels.length > 0) { - const displayedChannels = channels - .slice(0, 2) - .map((channel) => `#${channel.name}`) - .join(', '); - const moreChannelsIndicator = - channels.length > 2 ? '...' : ''; - return dataIndex > 0 - ? `, ${displayedChannels}${moreChannelsIndicator}` - : `${displayedChannels}${moreChannelsIndicator}`; + .map( + (item: { options: { channels: any } }, dataIndex: number) => { + const channels = item.options.channels; + if (channels && channels.length > 0) { + const displayedChannels = channels + .slice(0, 2) + .map((channel: { name: any }) => `#${channel.name}`) + .join(', '); + const moreChannelsIndicator = + channels.length > 2 ? '...' : ''; + return dataIndex > 0 + ? `, ${displayedChannels}${moreChannelsIndicator}` + : `${displayedChannels}${moreChannelsIndicator}`; + } + return ''; } - return ''; - }) - .filter((text) => text !== '') + ) + .filter((text: string) => text !== '') .join('')} variant="subtitle2" />
-
- + ); + case 'users': + return (
{ + .map((item: { options: { users: any } }) => { const users = item.options.users; if (users && users.length > 0) { const displayedUsers = users .slice(0, 2) - .map((user) => `@${user.ngu}`) + .map((user: { ngu: any }) => `@${user.ngu}`) .join(', '); const moreUsersIndicator = users.length > 2 ? '...' : ''; return `${displayedUsers}${moreUsersIndicator}`; } return ''; }) - .filter((text) => text !== '') + .filter((text: string) => text !== '') .join(', ')} variant="subtitle2" />
-
- + ); + case 'roles': + return (
{ + .map((item: { options: { roles: any } }) => { const roles = item.options.roles; if (roles && roles.length > 0) { const displayedRoles = roles .slice(0, 2) - .map((role) => role.name) + .map((role: { name: any }) => role.name) .join(', '); const moreRolesIndicator = roles.length > 2 ? '...' : ''; return `${displayedRoles}${moreRolesIndicator}`; } return ''; }) - .filter((text) => text !== '') + .filter((text: string) => text !== '') .join(', ')} variant="subtitle2" />
-
- + ); + case 'scheduledAt': + return (
-
- - handleClick(event, announcement.id)} - > - - - + handleClick(event, announcement.id)} + > + + + + handleEdit(announcement.id)}> + + Edit + + + + Delete + + + + ); + default: + return null; + } + }; + + const renderTableBody = () => { + if (isLoading) { + return ( + + - handleEdit(announcement.id)}> - - Edit - - - - Delete - - - + + +
+ ); + } + + return announcements.map((announcement, index) => ( + + {['title', 'channels', 'users', 'roles', 'scheduledAt', 'actions'].map( + (cellType, cellIndex, array) => ( + + {renderTableCell(announcement, cellType)} + + ) + )} )); }; diff --git a/src/pages/announcements/index.tsx b/src/pages/announcements/index.tsx index 12ec5276..a055b024 100644 --- a/src/pages/announcements/index.tsx +++ b/src/pages/announcements/index.tsx @@ -21,6 +21,7 @@ import TcAnnouncementsTable from '../../components/announcements/TcAnnouncements import TcDatePickerPopover from '../../components/shared/TcDatePickerPopover'; import TcAnnouncementsAlert from '../../components/announcements/TcAnnouncementsAlert'; import { useToken } from '../../context/TokenContext'; +import SimpleBackdrop from '../../components/global/LoadingBackdrop'; function Index() { const { retrieveAnnouncements, retrievePlatformById } = useAppStore(); @@ -28,6 +29,7 @@ function Index() { const { community } = useToken(); const [loading, setLoading] = useState(false); + const [isFirstLoad, setIsFirstLoad] = useState(true); const communityId = StorageService.readLocalStorage('community')?.id; @@ -64,6 +66,7 @@ function Index() { } catch (error) { } finally { setLoading(false); + if (isFirstLoad) setIsFirstLoad(false); } } }; @@ -75,7 +78,7 @@ function Index() { const [fetchedAnnouncements, setFetchedAnnouncements] = useState( { limit: 8, - page: 1, + page: page, results: [], totalPages: 0, totalResults: 0, @@ -96,6 +99,7 @@ function Index() { const handleDateChange = (date: Date | null) => { if (date) { setSelectedDate(date); + setPage(1); const fullDateTime = moment(date); setDateTimeDisplay(fullDateTime.format('D MMMM YYYY')); @@ -152,6 +156,10 @@ function Index() { setPage(selectedPage); }; + if (isFirstLoad && loading) { + return ; + } + return ( <> From db7a78cf46c302e38ec8d36cd3c7ad54e8aae461 Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 19 Jan 2024 15:16:04 +0300 Subject: [PATCH 29/30] fix issue --- src/components/announcements/TcAnnouncementsTable.tsx | 2 +- .../privateMessaageContainer/TcPrivateMessageContainer.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/announcements/TcAnnouncementsTable.tsx b/src/components/announcements/TcAnnouncementsTable.tsx index cce6de93..aed0614d 100644 --- a/src/components/announcements/TcAnnouncementsTable.tsx +++ b/src/components/announcements/TcAnnouncementsTable.tsx @@ -128,7 +128,7 @@ function TcAnnouncementsTable({ draft: any; data: any[]; scheduledAt: string | number | Date; - id: string | null; + id: string; }, cellType: any ) => { diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer.tsx index 7db12e65..2e29d779 100644 --- a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer.tsx +++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer.tsx @@ -256,7 +256,7 @@ function TcPrivateMessageContainer({
From 079cc76f82dd8b7fc1b9f5746cb9dc4ed5cf020e Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 19 Jan 2024 15:18:03 +0300 Subject: [PATCH 30/30] updaet button style --- .../TcConfirmSchaduledAnnouncementsDialog.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx index 027f9bec..7e7c9cec 100644 --- a/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx +++ b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx @@ -46,17 +46,6 @@ function TcConfirmSchaduledAnnouncementsDialog({ <> setConfirmSchadulerDialog(true)}