,
+ 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 {
+ 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);
+ }
+ };
+
+ const getAnnouncementTypeLabel = (type: string) => {
+ if (type === 'discord_public') {
+ return 'Public';
+ } else if (type === 'discord_private') {
+ return 'Private';
+ }
+ return 'Unknown';
+ };
+
+ const renderTableCell = (
+ announcement: {
+ draft: any;
+ data: any[];
+ scheduledAt: string | number | Date;
+ id: string;
+ },
+ cellType: any
+ ) => {
+ switch (cellType) {
+ case 'title':
+ return (
+
+
+
+ {announcement &&
+ announcement.data &&
+ announcement.data[0] &&
+ announcement.data[0].template
+ ? 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: string, index: React.Key | null | undefined) => (
+
+
+ {getAnnouncementTypeLabel(type)}
+
+ }
+ size="small"
+ sx={{
+ borderRadius: '4px',
+ borderColor: '#D1D1D1',
+ backgroundColor: 'white',
+ color: 'black',
+ }}
+ />
+ ))}
+
+
+ );
+ case 'channels':
+ return (
+
+ {
+ 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 '';
+ }
+ )
+ .filter((text: string) => text !== '')
+ .join('')}
+ variant="subtitle2"
+ />
+
+ );
+ // case 'users':
+ // return (
+ //
+ // {
+ // const users = item.options.users;
+ // if (users && users.length > 0) {
+ // const displayedUsers = users
+ // .slice(0, 2)
+ // .map((user: { ngu: any }) => `@${user.ngu}`)
+ // .join(', ');
+ // const moreUsersIndicator = users.length > 2 ? '...' : '';
+ // return `${displayedUsers}${moreUsersIndicator}`;
+ // }
+ // return '';
+ // })
+ // .filter((text: string) => text !== '')
+ // .join(', ')}
+ // variant="subtitle2"
+ // />
+ //
+ // );
+ // case 'roles':
+ // return (
+ //
+ // {
+ // const roles = item.options.roles;
+ // if (roles && roles.length > 0) {
+ // const displayedRoles = roles
+ // .slice(0, 2)
+ // .map((role: { name: any }) => role.name)
+ // .join(', ');
+ // const moreRolesIndicator = roles.length > 2 ? '...' : '';
+ // return `${displayedRoles}${moreRolesIndicator}`;
+ // }
+ // return '';
+ // })
+ // .filter((text: string) => text !== '')
+ // .join(', ')}
+ // variant="subtitle2"
+ // />
+ //
+ // );
+ case 'scheduledAt':
+ return (
+
+
+
+ );
+ case 'actions':
+ return (
+ <>
+ handleClick(event, announcement.id)}
+ >
+
+
+
+ >
+ );
+ default:
+ return null;
+ }
+ };
+
+ const renderTableBody = () => {
+ if (isLoading) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ {announcements.map((announcement, index) => (
+
+ {['title', 'channels', 'scheduledAt', 'actions'].map(
+ (cellType, cellIndex, array) => (
+
+ {renderTableCell(announcement, cellType)}
+
+ )
+ )}
+
+ ))}
+
+ );
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {/*
+
+ */}
+ {/*
+
+ */}
+
+
+
+
+
+
+ {renderTableBody()}
+
+
+
+
setDeleteConfirmDialogOpen(false)}
+ />
+
+
+
+
+
+
+ setDeleteConfirmDialogOpen(false)}
+ />
+
+ selectedAnnouncementId &&
+ handleDeleteAnnouncements(selectedAnnouncementId)
+ }
+ />
+
+
+
+ >
+ }
+ open={deleteConfirmDialogOpen}
+ />
+ >
+ );
+}
+
+export default TcAnnouncementsTable;
diff --git a/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.spec.tsx b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.spec.tsx
new file mode 100644
index 00000000..e3e0ce30
--- /dev/null
+++ b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.spec.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import TcConfirmSchaduledAnnouncementsDialog from './TcConfirmSchaduledAnnouncementsDialog';
+
+const mockHandleCreateAnnouncements = jest.fn();
+
+const defaultProps = {
+ buttonLabel: 'Select Date for Announcement',
+ selectedChannels: [{ id: '1', label: 'General' }],
+ schaduledDate: '2024-01-20T12:00:00',
+ isDisabled: false,
+ handleCreateAnnouncements: mockHandleCreateAnnouncements,
+};
+
+test('renders the dialog with button and calls handleCreateAnnouncements when confirmed', () => {
+ const { getByText, getByTestId } = render(
+
+ );
+
+ const button = getByText('Select Date for Announcement');
+ expect(button).toBeInTheDocument();
+
+ fireEvent.click(button);
+
+ const dialogTitle = getByText('Confirm Schedule');
+ expect(dialogTitle).toBeInTheDocument();
+
+ const confirmButton = getByText('Confirm');
+ expect(confirmButton).toBeInTheDocument();
+ fireEvent.click(confirmButton);
+
+ expect(mockHandleCreateAnnouncements).toHaveBeenCalledWith(false);
+});
diff --git a/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx
new file mode 100644
index 00000000..3e06c7ac
--- /dev/null
+++ b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx
@@ -0,0 +1,178 @@
+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';
+import moment from 'moment';
+import { IRoles, IUser } from '../../utils/interfaces';
+
+interface ITcConfirmSchaduledAnnouncementsDialogProps {
+ buttonLabel: 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] hh:mm A');
+
+ 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);
+
+ return (
+ <>
+ setConfirmSchadulerDialog(true)}
+ />
+
+
+
setConfirmSchadulerDialog(false)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {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);
+ handleCreateAnnouncements(false);
+ }}
+ sx={{ width: '100%' }}
+ />
+
+
+ >
+ }
+ open={confirmSchadulerDialog}
+ />
+ >
+ );
+}
+
+export default TcConfirmSchaduledAnnouncementsDialog;
diff --git a/src/components/announcements/TcTimeZone.spec.tsx b/src/components/announcements/TcTimeZone.spec.tsx
new file mode 100644
index 00000000..351de2bc
--- /dev/null
+++ b/src/components/announcements/TcTimeZone.spec.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { render, fireEvent, waitFor } from '@testing-library/react';
+import TcTimeZone from './TcTimeZone';
+
+test('should handle zone selection', async () => {
+ const handleZoneFunction = jest.fn();
+
+ 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
new file mode 100644
index 00000000..b9fdc857
--- /dev/null
+++ b/src/components/announcements/TcTimeZone.tsx
@@ -0,0 +1,129 @@
+import React, { useEffect, 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();
+
+interface ITcTimeZoneProps {
+ handleZone: (zone: string) => void;
+}
+function TcTimeZone({ handleZone }: ITcTimeZoneProps) {
+ 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);
+ };
+
+ useEffect(() => {
+ handleZone(activeZone);
+ }, [activeZone]);
+
+ 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/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/privateMessaageContainer/TcPrivateMessageContainer.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer.tsx
new file mode 100644
index 00000000..5d7f4a9f
--- /dev/null
+++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer.tsx
@@ -0,0 +1,280 @@
+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 { 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';
+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',
+ RoleOnly = 'Role Only',
+ UserOnly = 'User Only',
+}
+
+export interface ITcPrivateMessageContainerProps {
+ isEdit?: boolean;
+ privateAnnouncementsData?: DiscordData[] | undefined;
+ handlePrivateAnnouncements: ({
+ message,
+ selectedRoles,
+ selectedUsers,
+ }: {
+ message: string;
+ selectedRoles?: IRoles[];
+ selectedUsers?: IUser[];
+ }) => void;
+}
+
+function TcPrivateMessageContainer({
+ handlePrivateAnnouncements,
+ isEdit = false,
+ privateAnnouncementsData,
+}: ITcPrivateMessageContainerProps) {
+ const [privateMessage, setPrivateMessage] = useState(false);
+ const [messageType, setMessageType] = useState(MessageType.Both);
+ const [selectedUsers, setSelectedUsers] = useState([]);
+ const [selectedRoles, setSelectedRoles] = useState([]);
+
+ const [message, setMessage] = useState('');
+
+ const handleChange = (event: React.ChangeEvent) => {
+ setMessage(event.target.value);
+ };
+
+ const handlePrivateMessageChange = (
+ event: React.ChangeEvent
+ ) => {
+ setPrivateMessage(event.target.checked);
+ };
+
+ const messageTypesArray = Object.values(MessageType);
+
+ const isPreviewDialogEnabled = message.length > 0 && privateMessage == true;
+
+ const getSelectedRolesLabels = () => {
+ return selectedRoles.map((role) => role.name || '');
+ };
+
+ const selectedRolesLables = getSelectedRolesLabels();
+
+ const getSelectedUsersLabels = () => {
+ return selectedUsers.map((user) => user.ngu || '');
+ };
+
+ 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]);
+
+ 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 (
+
+
+
+
+
+
+
+
+ }
+ label={
+
+
+
+ }
+ />
+
+
+
+ {messageTypesArray.map((el) => (
+ setMessageType(el)}
+ />
+ ))}
+
+
+
+
+ {privateMessage && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
+
+export default TcPrivateMessageContainer;
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..6be77de4
--- /dev/null
+++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.tsx
@@ -0,0 +1,130 @@
+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/privateMessaageContainer/TcRolesAutoComplete.tsx b/src/components/announcements/create/privateMessaageContainer/TcRolesAutoComplete.tsx
new file mode 100644
index 00000000..9fa74550
--- /dev/null
+++ b/src/components/announcements/create/privateMessaageContainer/TcRolesAutoComplete.tsx
@@ -0,0 +1,220 @@
+import React, { useEffect, useState } from 'react';
+import { useToken } from '../../../../context/TokenContext';
+import useAppStore from '../../../../store/useStore';
+import { FetchedData, IRoles } from '../../../../utils/interfaces';
+import { debounce } from '../../../../helpers/helper';
+import TcAutocomplete from '../../../shared/TcAutocomplete';
+import { Chip, CircularProgress } from '@mui/material';
+
+interface ITcRolesAutoCompleteProps {
+ isEdit?: boolean;
+ privateSelectedRoles?: IRoles[];
+ isDisabled: boolean;
+ handleSelectedUsers: (roles: IRoles[]) => void;
+}
+
+function TcRolesAutoComplete({
+ isEdit = false,
+ privateSelectedRoles,
+ 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 [isInitialized, setIsInitialized] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const fetchDiscordRoles = async (
+ platformId: string,
+ page?: number,
+ limit?: number,
+ name?: string
+ ) => {
+ try {
+ setIsLoading(true);
+
+ 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) {
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ 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, 100);
+ } else {
+ debouncedFetchDiscordRoles(platformId, 1, 100, 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]);
+
+ useEffect(() => {
+ if (isEdit && !isInitialized) {
+ if (privateSelectedRoles !== undefined) {
+ setSelectedRoles(privateSelectedRoles);
+ } else {
+ setSelectedRoles([]);
+ }
+ setIsInitialized(true);
+ }
+ }, [privateSelectedRoles, isEdit, isInitialized]);
+
+ return (
+ option.name}
+ label={'Select Role(s)'}
+ multiple={true}
+ loading={isLoading}
+ loadingText={
+
+
+
+ }
+ disabled={isDisabled}
+ value={selectedRoles}
+ onChange={handleChange}
+ onInputChange={handleSearchChange}
+ isOptionEqualToValue={(option, value) => option.roleId === value.roleId}
+ disableCloseOnSelect
+ renderOption={(props, option) => (
+
+ {option.name}
+
+ )}
+ renderTags={(value, getTagProps) =>
+ value.map((option, index) => (
+
+
+ {option.name}
+
+ }
+ size="small"
+ sx={{
+ borderRadius: '4px',
+ borderColor: '#D1D1D1',
+ backgroundColor: 'white',
+ color: 'black',
+ }}
+ {...getTagProps({ 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..7ffc8edc
--- /dev/null
+++ b/src/components/announcements/create/privateMessaageContainer/TcUsersAutoComplete.tsx
@@ -0,0 +1,261 @@
+import React, { useEffect, useState } from 'react';
+import { useToken } from '../../../../context/TokenContext';
+import useAppStore from '../../../../store/useStore';
+import { FetchedData, IUser } from '../../../../utils/interfaces';
+import { debounce, truncateCenter } from '../../../../helpers/helper';
+import TcAutocomplete from '../../../shared/TcAutocomplete';
+import { Chip, CircularProgress } from '@mui/material';
+import TcAvatar from '../../../shared/TcAvatar';
+import TcText from '../../../shared/TcText';
+import { conf } from '../../../../configs';
+
+interface ITcUsersAutoCompleteProps {
+ isEdit?: boolean;
+ privateSelectedUsers?: IUser[];
+ isDisabled: boolean;
+ handleSelectedUsers: (users: IUser[]) => void;
+}
+
+function TcUsersAutoComplete({
+ isEdit = false,
+ privateSelectedUsers,
+ 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 [filteredUsersByName, setFilteredUsersByName] = useState('');
+ const [isInitialized, setIsInitialized] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const fetchDiscordUsers = async (
+ platformId: string,
+ page?: number,
+ limit?: number,
+ ngu?: string
+ ) => {
+ try {
+ setIsLoading(true);
+
+ const fetchedUsers = await retrievePlatformProperties({
+ platformId,
+ ngu: ngu,
+ property: 'guildMember',
+ page: page,
+ limit: limit,
+ });
+
+ if (ngu) {
+ setFilteredUsersByName(ngu);
+ 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) {
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (!platformId) return;
+ fetchDiscordUsers(platformId, fetchedUsers.page, fetchedUsers.limit);
+ }, []);
+
+ const debouncedFetchDiscordUsers = debounce(fetchDiscordUsers, 700);
+
+ const handleClearAll = () => {
+ if (!platformId) return;
+ fetchDiscordUsers(platformId, fetchedUsers.page, fetchedUsers.limit);
+ };
+
+ const handleSearchChange = (event: React.SyntheticEvent) => {
+ const target = event.target as HTMLInputElement;
+ const inputValue = target.value;
+
+ if (!platformId) return;
+
+ if (inputValue === '') {
+ setFilteredUsersByName('');
+ 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]);
+
+ useEffect(() => {
+ if (isEdit && !isInitialized) {
+ if (privateSelectedUsers !== undefined) {
+ setSelectedUsers(privateSelectedUsers);
+ } else {
+ setSelectedUsers([]);
+ }
+ setIsInitialized(true);
+ }
+ }, [privateSelectedUsers, isEdit, isInitialized]);
+
+ return (
+ option.ngu}
+ label={'Select User(s)'}
+ multiple={true}
+ loading={isLoading}
+ loadingText={
+
+
+
+ }
+ disabled={isDisabled}
+ value={selectedUsers}
+ onChange={handleChange}
+ onInputChange={(event, value, reason) => {
+ if (reason === 'clear') {
+ handleClearAll();
+ } else {
+ handleSearchChange(event);
+ }
+ }}
+ isOptionEqualToValue={(option, value) =>
+ option.discordId === value.discordId
+ }
+ disableCloseOnSelect
+ renderOption={(props, option) => (
+
+
+
+
+
+
+
+ )}
+ renderTags={(value, getTagProps) =>
+ value.map((option, index) => (
+
+
+
+
+
+
+
+ }
+ size="small"
+ sx={{
+ borderRadius: '4px',
+ borderColor: '#D1D1D1',
+ backgroundColor: 'white',
+ color: 'black',
+ }}
+ {...getTagProps({ index })}
+ />
+ ))
+ }
+ textFieldProps={{ variant: 'filled' }}
+ ListboxProps={{
+ onScroll: handleScroll,
+ style: {
+ maxHeight: '280px',
+ overflow: 'auto',
+ },
+ }}
+ />
+ );
+}
+
+export default TcUsersAutoComplete;
diff --git a/src/components/announcements/create/privateMessaageContainer/index.ts b/src/components/announcements/create/privateMessaageContainer/index.ts
new file mode 100644
index 00000000..cfd2afd6
--- /dev/null
+++ b/src/components/announcements/create/privateMessaageContainer/index.ts
@@ -0,0 +1,3 @@
+import { default as TcPrivateMessaageContainer } from './TcPrivateMessageContainer';
+
+export default TcPrivateMessaageContainer;
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..88d1b3d1
--- /dev/null
+++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.spec.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import TcPublicMessageContainer from './TcPublicMessageContainer';
+import { TokenContext } from '../../../../context/TokenContext';
+
+const mockToken = {
+ accessToken: 'mockAccessToken',
+ refreshToken: 'mockRefreshToken',
+};
+
+const mockCommunity = {
+ name: 'Test Community',
+ platforms: [],
+ id: 'mockCommunityId',
+ users: [],
+ avatarURL: 'mockAvatarURL',
+};
+
+const mockTokenContextValue = {
+ token: mockToken,
+ community: mockCommunity,
+ updateToken: jest.fn(),
+ updateCommunity: jest.fn(),
+ deleteCommunity: jest.fn(),
+ clearToken: jest.fn(),
+};
+
+describe('TcPublicMessageContainer', () => {
+ beforeEach(() => {
+ render(
+
+
+
+ );
+ });
+
+ it('renders the "Public Message" text', () => {
+ expect(screen.getByText(/Public Message/i)).toBeInTheDocument();
+ });
+
+ it('renders the message about bot distribute', () => {
+ const message =
+ /Our bot will distribute the announcement through selected channels with the required access to share the designated message./i;
+ expect(screen.getByText(message)).toBeInTheDocument();
+ });
+
+ it('renders the "Write message here:" text', () => {
+ expect(screen.getByText(/Write message here:/i)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.tsx
new file mode 100644
index 00000000..fba55ba9
--- /dev/null
+++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.tsx
@@ -0,0 +1,205 @@
+import React, { useContext, useEffect, 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 } 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';
+import { DiscordData } from '../../../../pages/announcements/edit-announcements';
+import TcPermissionHints from '../../../global/TcPermissionHints';
+import TcButton from '../../../shared/TcButton';
+
+export interface FlattenedChannel {
+ id: string;
+ label: string;
+}
+
+export interface ITcPublicMessageContainerProps {
+ isEdit?: boolean;
+ publicAnnouncementsData?: DiscordData | undefined;
+ handlePublicAnnouncements: ({
+ message,
+ selectedChannels,
+ }: {
+ message: string;
+ selectedChannels: FlattenedChannel[] | [];
+ }) => void;
+}
+
+function TcPublicMessageContainer({
+ handlePublicAnnouncements,
+ isEdit = false,
+ publicAnnouncementsData,
+}: ITcPublicMessageContainerProps) {
+ const channelContext = useContext(ChannelContext);
+
+ const { channels, selectedSubChannels } = channelContext;
+ const [hasInteracted, setHasInteracted] = useState(false);
+ const [showError, setShowError] = useState(false);
+ const [isDropdownVisible, setIsDropdownVisible] = useState(false);
+
+ 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(
+ []
+ );
+ const [confirmedSelectedChannels, setConfirmedSelectedChannels] =
+ useState(false);
+
+ 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(() => {
+ if (confirmedSelectedChannels) {
+ handlePublicAnnouncements({ message, selectedChannels });
+ } else {
+ handlePublicAnnouncements({ message, selectedChannels: [] });
+ }
+ }, [message, selectedChannels, confirmedSelectedChannels]);
+
+ useEffect(() => {
+ 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,
+ })
+ );
+ setConfirmedSelectedChannels(true);
+ setSelectedChannels(formattedChannels);
+ setMessage(publicAnnouncementsData.template);
+ }
+ }
+ }, [isEdit, publicAnnouncementsData]);
+
+ const handleSaveChannels = () => {
+ setConfirmedSelectedChannels(true);
+ setIsDropdownVisible(false);
+ setShowError(hasInteracted && selectedChannels.length === 0);
+ };
+
+ const toggleDropdownVisibility = () => {
+ setHasInteracted(true);
+ setIsDropdownVisible(!isDropdownVisible);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
channel.label)}
+ />
+
+
+
+ Select Channels
+
+ (selected as FlattenedChannel[])
+ .map((channel) => `#${channel.label}`)
+ .join(', ')
+ }
+ >
+
+
+ {showError && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default TcPublicMessageContainer;
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..15274c87
--- /dev/null
+++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx
@@ -0,0 +1,102 @@
+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;
+ selectedChannels: string[];
+ isPreviewDialogEnabled: boolean;
+}
+
+function TcPublicMessagePreviewDialog({
+ textMessage,
+ selectedChannels,
+ isPreviewDialogEnabled,
+}: ITcPublicMessagePreviewDialogProps) {
+ const [isPreviewDialogOpen, setPreviewDialogOpen] = useState(false);
+ return (
+ <>
+ setPreviewDialogOpen(true)}
+ />
+
+
+
setPreviewDialogOpen(false)}
+ />
+
+
+
+
+
+ {selectedChannels &&
+ selectedChannels.map((channel, index, array) => (
+
+ {'#'}
+
+ {index < array.length - 1 && ', '}
+
+ ))}
+
+
+
+ setPreviewDialogOpen(false)}
+ sx={{ width: '100%' }}
+ />
+
+
+ >
+ }
+ open={isPreviewDialogOpen}
+ />
+ >
+ );
+}
+
+export default TcPublicMessagePreviewDialog;
diff --git a/src/components/announcements/create/publicMessageContainer/index.ts b/src/components/announcements/create/publicMessageContainer/index.ts
new file mode 100644
index 00000000..53cda254
--- /dev/null
+++ b/src/components/announcements/create/publicMessageContainer/index.ts
@@ -0,0 +1,3 @@
+import { default as TcPublicMessageContainer } from './TcPublicMessageContainer';
+
+export default TcPublicMessageContainer;
diff --git a/src/components/announcements/create/scheduleAnnouncement/TcDateTimePopover.tsx b/src/components/announcements/create/scheduleAnnouncement/TcDateTimePopover.tsx
new file mode 100644
index 00000000..fbe8f715
--- /dev/null
+++ b/src/components/announcements/create/scheduleAnnouncement/TcDateTimePopover.tsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import { StaticDatePicker } from '@mui/x-date-pickers/StaticDatePicker';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
+import { StaticTimePicker } from '@mui/x-date-pickers';
+import { FiCalendar } from 'react-icons/fi';
+import { MdAccessTime } from 'react-icons/md';
+import TcPopover from '../../../shared/TcPopover';
+import TcTabs from '../../../shared/TcTabs';
+import TcTab from '../../../shared/TcTabs/TcTab';
+
+interface IDateTimePopoverProps {
+ open: boolean;
+ anchorEl: HTMLButtonElement | null;
+ onClose: () => 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..b07aef74
--- /dev/null
+++ b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.spec.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import TcScheduleAnnouncement from './TcScheduleAnnouncement';
+
+describe('TcScheduleAnnouncement Tests', () => {
+ // Mock functions for the new props
+ const mockHandleScheduledDate = jest.fn();
+ const mockSetIsDateValid = jest.fn();
+
+ test('renders the component without crashing', () => {
+ render(
+
+ );
+
+ // Since the initial text is "Select Date for Announcement", we should assert this text.
+ expect(
+ screen.getByText('Select Date for Announcement')
+ ).toBeInTheDocument();
+ });
+
+ test('initially displays the calendar icon', () => {
+ render(
+
+ );
+
+ // Assuming your TcButton component renders an icon, this test checks for its presence.
+ const calendarIcon = screen.getByTestId('MdCalendarMonth');
+ expect(calendarIcon).toBeInTheDocument();
+ });
+
+ test('displays the button to open date-time popover', () => {
+ render(
+
+ );
+
+ // Check if the button that is supposed to open the date-time popover is rendered.
+ const button = screen.getByRole('button', {
+ name: /select date for announcement/i,
+ });
+ expect(button).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..394f88f4
--- /dev/null
+++ b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx
@@ -0,0 +1,137 @@
+import React, { useEffect, 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 TcDateTimePopover from './TcDateTimePopover';
+import { validateDateTime } from '../../../../helpers/helper';
+
+export interface ITcScheduleAnnouncementProps {
+ isEdit?: boolean;
+ preSelectedTime?: string;
+ handleSchaduledDate: ({ selectedTime }: { selectedTime: string }) => void;
+ isDateValid: boolean;
+ setIsDateValid: (isValid: boolean) => void;
+}
+
+function TcScheduleAnnouncement({
+ isEdit = false,
+ preSelectedTime,
+ handleSchaduledDate,
+ isDateValid,
+ setIsDateValid,
+}: ITcScheduleAnnouncementProps) {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const [activeTab, setActiveTab] = useState(0);
+ const [selectedDate, setSelectedDate] = useState(null);
+ const [selectedTime, setSelectedTime] = useState(null);
+
+ const [dateTimeDisplay, setDateTimeDisplay] = useState(
+ 'Select Date for Announcement'
+ );
+
+ 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 isValid = validateDateTime(date, selectedTime);
+ setIsDateValid(isValid);
+ }
+ };
+
+ const handleTimeChange = (time: Date | null) => {
+ if (time) {
+ setSelectedTime(time);
+
+ if (selectedDate) {
+ const fullDateTime = moment(selectedDate).set({
+ hour: time.getHours(),
+ minute: time.getMinutes(),
+ });
+ setDateTimeDisplay(fullDateTime.format('D MMMM YYYY @ hh:mm A'));
+ }
+ const isValid = validateDateTime(selectedDate, time);
+ setIsDateValid(isValid);
+ }
+ };
+
+ useEffect(() => {
+ if (!selectedDate || !selectedTime) return;
+
+ const fullDateTime = moment(selectedDate).set({
+ hour: selectedTime.getHours(),
+ minute: selectedTime.getMinutes(),
+ });
+
+ const fullDateTimeUTC = fullDateTime.utc();
+
+ const formattedUTCDate = fullDateTimeUTC.format();
+
+ handleSchaduledDate({ selectedTime: formattedUTCDate });
+ }, [selectedDate, selectedTime]);
+
+ useEffect(() => {
+ if (isEdit && preSelectedTime) {
+ const dateTime = moment(preSelectedTime);
+ const date = dateTime.toDate();
+ setSelectedDate(date);
+ setSelectedTime(date);
+ setDateTimeDisplay(dateTime.format('D MMMM YYYY @ hh:mm A'));
+ setIsDateValid(validateDateTime(date, date));
+ }
+ }, [isEdit, preSelectedTime]);
+
+ return (
+
+
+
+
+
+
+
+
+
}
+ disableElevation={true}
+ className="border border-black bg-gray-100 shadow-md"
+ sx={{ color: 'black', height: '2.4rem', paddingX: '1rem' }}
+ aria-describedby={id}
+ onClick={handleOpen}
+ />
+ {!isDateValid && (
+
+ )}
+
+
+
+ );
+}
+
+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..8fac03f6
--- /dev/null
+++ b/src/components/announcements/create/selectPlatform/TcSelectPlatform.tsx
@@ -0,0 +1,56 @@
+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: ,
+ },
+];
+
+interface ITcSelectPlatformProps {
+ isEdit: boolean;
+}
+
+function TcSelectPlatform({ isEdit }: ITcSelectPlatformProps) {
+ 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/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/communitySettings/platform/TcPlatformChannelList.tsx b/src/components/communitySettings/platform/TcPlatformChannelList.tsx
index 9baa339d..13bd52e4 100644
--- a/src/components/communitySettings/platform/TcPlatformChannelList.tsx
+++ b/src/components/communitySettings/platform/TcPlatformChannelList.tsx
@@ -95,7 +95,8 @@ function TcPlatformChannelList({
}
disabled={channel?.subChannels?.some(
(subChannel) =>
- !subChannel.canReadMessageHistoryAndViewChannel
+ !subChannel.canReadMessageHistoryAndViewChannel ||
+ !subChannel.announcementAccess
)}
/>
}
diff --git a/src/components/global/.CustomTab.tsx.swp b/src/components/global/.CustomTab.tsx.swp
new file mode 100644
index 00000000..1c19527f
Binary files /dev/null and b/src/components/global/.CustomTab.tsx.swp differ
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 (
-
- );
-}
-
-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/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/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/src/components/global/TcPermissionHints/TcPermissionHints.spec.tsx b/src/components/global/TcPermissionHints/TcPermissionHints.spec.tsx
new file mode 100644
index 00000000..13710ad1
--- /dev/null
+++ b/src/components/global/TcPermissionHints/TcPermissionHints.spec.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import PermissionHints from './TcPermissionHints';
+describe('PermissionHints Component', () => {
+ test('renders PermissionHints component', () => {
+ render( );
+ expect(screen.getByText('Access Settings')).toBeInTheDocument();
+ expect(screen.getByText('Server Level')).toBeInTheDocument();
+ expect(screen.getByText('Category Level')).toBeInTheDocument();
+ expect(screen.getByText('Channel Level')).toBeInTheDocument();
+ });
+
+ test('initial active category is Access Settings', async () => {
+ render( );
+ await waitFor(() => {
+ expect(
+ screen.getByText('What does “Read” and “Write” access mean?')
+ ).toBeInTheDocument();
+ });
+ });
+
+ test('clicking on a category button changes active category to Server Level', async () => {
+ render( );
+ const serverLevelButton = screen.getByText('Server Level');
+ userEvent.click(serverLevelButton);
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ 'Please note that your platform’s permission settings enable the above permission controls'
+ )
+ ).toBeInTheDocument();
+ });
+ });
+
+ test('clicking on a category button changes active category to Category Level', async () => {
+ render( );
+ const categoryLevelButton = screen.getByText('Category Level');
+ userEvent.click(categoryLevelButton);
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ 'Please note that Category-level permissions override Server-level permissions'
+ )
+ ).toBeInTheDocument();
+ });
+ });
+
+ test('clicking on a category button changes active category to Channel Level', async () => {
+ render( );
+
+ const channelLevelButton = screen.getByText('Channel Level');
+
+ userEvent.click(channelLevelButton);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ 'Please note that Channel-level permissions override Category-level permissions, which in turn override Server-level permissions'
+ )
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/components/global/TcPermissionHints/TcPermissionHints.tsx b/src/components/global/TcPermissionHints/TcPermissionHints.tsx
new file mode 100644
index 00000000..10d023d7
--- /dev/null
+++ b/src/components/global/TcPermissionHints/TcPermissionHints.tsx
@@ -0,0 +1,329 @@
+import React, { useState } from 'react';
+import TcButtonGroup from '../../shared/TcButtonGroup';
+import TcButton from '../../shared/TcButton';
+import clsx from 'clsx';
+import TcText from '../../shared/TcText';
+
+const permissionCategories = [
+ 'Access Settings',
+ 'Server Level',
+ 'Category Level',
+ 'Channel Level',
+];
+
+function PermissionHints() {
+ const [activeCategory, setActiveCategory] =
+ useState('Access Settings');
+
+ const handleButtonClick = (category: string) => {
+ setActiveCategory(category);
+ };
+
+ const getDescription = (category: string) => {
+ switch (category) {
+ case 'Access Settings':
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ case 'Server Level':
+ return (
+
+
+
+
+
+ Navigate to the “Server Settings” in the top-left
+ corner of Discord
+ >
+ }
+ variant="body2"
+ />
+
+
+
+ Select “Role/Members” (left sidebar), and then in
+ the middle of the screen check Advanced permissions
+ >
+ }
+ variant="body2"
+ />
+
+
+
+ Then select “TogetherCrew” and under Advanced
+ Permissions, make sure that the following are marked as
+ [✓]
+ >
+ }
+ variant="body2"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ Finally: Click on the Refresh List button on this page
+ and select the channels that have now been made available to
+ you
+ >
+ }
+ variant="body2"
+ />
+
+ );
+ case 'Category Level':
+ return (
+
+
+
+
+
+ Navigate to the “Edit Category” in the top-left
+ corner of Discord
+ >
+ }
+ variant="body2"
+ />
+
+
+
+ Select “Permissions” (left sidebar), and then in
+ the middle of the screen check Advanced permissions
+ >
+ }
+ variant="body2"
+ />
+
+
+
+ Then select “TogetherCrew” and under Advanced
+ Permissions, make sure that the following are marked as
+ [✓]
+ >
+ }
+ variant="body2"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ Finally: Click on the Refresh List button on this page
+ and select the channels that have now been made available to
+ you
+ >
+ }
+ variant="body2"
+ />
+
+ );
+ case 'Channel Level':
+ return (
+
+
+
+
+
+ Navigate to the settings for a specific channel (select
+ the wheel on the right of the channel name)
+ >
+ }
+ variant="body2"
+ />
+
+
+
+ Select “Permissions” (left sidebar), and then in
+ the middle of the screen check Advanced permissions
+ >
+ }
+ variant="body2"
+ />
+
+
+
+ Then select “TogetherCrew” and under Advanced
+ Permissions, make sure that the following are marked as
+ [✓]
+ >
+ }
+ variant="body2"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ Finally: Click on the Refresh List button on this page
+ and select the channels that have now been made available to
+ you
+ >
+ }
+ variant="body2"
+ />
+
+ );
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+
+ {permissionCategories.map((category) => (
+ handleButtonClick(category)}
+ className={clsx(
+ 'border',
+ category === activeCategory
+ ? 'bg-secondary text-white border-secondary'
+ : 'border-secondary bg-white text-secondary'
+ )}
+ sx={{
+ width: 'auto',
+ padding: {
+ xs: 'auto',
+ sm: '0.4rem 1rem',
+ },
+ }}
+ />
+ ))}
+
+
{getDescription(activeCategory)}
+
+ );
+}
+
+export default PermissionHints;
diff --git a/src/components/global/TcPermissionHints/index.ts b/src/components/global/TcPermissionHints/index.ts
new file mode 100644
index 00000000..3bfc5a13
--- /dev/null
+++ b/src/components/global/TcPermissionHints/index.ts
@@ -0,0 +1,3 @@
+import { default as TcPermissionHints } from './TcPermissionHints';
+
+export default TcPermissionHints;
diff --git a/src/components/layouts/Sidebar.tsx b/src/components/layouts/Sidebar.tsx
index 3fb55c95..64cfeffa 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: 'Smart Announcements',
+ path: '/announcements',
+ icon: (
+
+ ),
+ },
{
name: 'Community Settings',
path: '/community-settings',
@@ -83,7 +93,7 @@ const Sidebar = () => {
>
{el.icon}
- {el.name}
+ {el.name}
));
diff --git a/src/components/layouts/xs/SidebarXs.tsx b/src/components/layouts/xs/SidebarXs.tsx
index bd95d84d..8a1791d8 100644
--- a/src/components/layouts/xs/SidebarXs.tsx
+++ b/src/components/layouts/xs/SidebarXs.tsx
@@ -16,7 +16,7 @@ import Link from 'next/link';
import { Drawer } from '@mui/material';
import { FaBars } from 'react-icons/fa';
-import { MdKeyboardBackspace } from 'react-icons/md';
+import { MdKeyboardBackspace, MdOutlineAnnouncement } from 'react-icons/md';
import { conf } from '../../../configs';
import { FiSettings } from 'react-icons/fi';
import { useToken } from '../../../context/TokenContext';
@@ -65,12 +65,21 @@ const Sidebar = () => {
/>
),
},
+ {
+ name: 'Smart Announcements',
+ path: '/announcements',
+ icon: (
+
+ ),
+ },
{
name: 'Community Settings',
path: '/community-settings',
icon: (
),
},
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,
-};
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
-
-
-
-
-
-
-
- Use data to improve onboarding
-
-
-
-
-
-
-
-
- Explore all the metrics that determine the health of your
- community
-
-
-
-
-
Read our research on
-
-
-
- Community Health
-
-
-
-
-
-
-
- Monitor members who disengage and take action to bring them back
-
-
-
-
-
-
-
- Benchmark your metrics and learn from others
-
-
-
-
-
-
-
- >
- );
-};
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/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?
-
-
-
-
-
-
- Navigate to the channel you want to import on{' '}
-
- Discord
-
-
-
- Go to the settings for that specific channel (select the
- wheel on the right of the channel name)
-
-
- Select Permissions (left sidebar), and then in the
- middle of the screen check Advanced permissions
-
-
- With the TogetherCrew Bot selected, under Advanced
- Permissions, make sure that [View channel] and [Read message
- history] are marked as [✓]
-
-
- Select the plus sign to the right of Roles/Members and under
- members select TogetherCrew bot
-
-
- Click on the Refresh List button on this window and
- select the new channels
-
-
-
-
-
-
-
-
-
-
-
{' '}
-
- );
-}
-
-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
-
-
-
-
- ) : (
- <>>
- )}
-
- {guilds.length >= 1 ? (
-
- It will be possible to connect more communities soon.
-
- }
- arrow
- placement="right"
- >
-
- Discord
-
-
-
-
- ) : (
-
connectNewGuild()}
- >
- Discord
-
-
-
- )}
-
-
-
-
- >
- );
-}
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.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?.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 (
-
-
-
-
- );
-}
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/components/shared/TcBreadcrumbs.tsx b/src/components/shared/TcBreadcrumbs.tsx
index 465b792b..4abdae16 100644
--- a/src/components/shared/TcBreadcrumbs.tsx
+++ b/src/components/shared/TcBreadcrumbs.tsx
@@ -1,37 +1,13 @@
-/**
- * `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 { useRouter } from 'next/router';
import TcLink from './TcLink';
-import { MdOutlineKeyboardArrowLeft } from 'react-icons/md';
+import { MdChevronRight } from 'react-icons/md';
+import TcText from './TcText';
interface BreadcrumbItem {
label: string;
- path: string;
+ path?: string;
}
interface TcBreadcrumbsProps {
@@ -50,22 +26,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/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/TcDatePickerPopover.tsx b/src/components/shared/TcDatePickerPopover.tsx
new file mode 100644
index 00000000..6e00729a
--- /dev/null
+++ b/src/components/shared/TcDatePickerPopover.tsx
@@ -0,0 +1,55 @@
+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';
+import TcButton from './TcButton';
+
+interface ITcDatePickerPopoverProps {
+ open: boolean;
+ anchorEl: HTMLElement | null;
+ onClose: () => void;
+ selectedDate: Date | null;
+ onDateChange: (date: Date | null) => void;
+ onResetDate: () => void;
+}
+
+function TcDatePickerPopover({
+ open,
+ anchorEl,
+ onClose,
+ selectedDate,
+ onDateChange,
+ onResetDate,
+}: ITcDatePickerPopoverProps) {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+export default TcDatePickerPopover;
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/TcPagination/TcPagination.spec.tsx b/src/components/shared/TcPagination/TcPagination.spec.tsx
new file mode 100644
index 00000000..772ec1be
--- /dev/null
+++ b/src/components/shared/TcPagination/TcPagination.spec.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import TcPagination from './TcPagination';
+
+describe('TcPagination', () => {
+ 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/TcSelect/TcSelect.tsx b/src/components/shared/TcSelect/TcSelect.tsx
index d911cd2d..124904ca 100644
--- a/src/components/shared/TcSelect/TcSelect.tsx
+++ b/src/components/shared/TcSelect/TcSelect.tsx
@@ -1,47 +1,57 @@
+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;
+ }>;
+ children?: React.ReactNode;
}
-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,
+ children,
+ ...props
+}: ITcSelectProps): ReactElement {
return (
-
- {options.map((option) => (
-
- {option.label}
-
- ))}
+
+ {options && options.length > 0
+ ? options.map((option) => (
+
+
+ {option.icon && React.cloneElement(option.icon)}
+
+
+
+ ))
+ : children}
);
}
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;
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..f301c81e
--- /dev/null
+++ b/src/components/shared/TcTableContainer/TcTableHead.tsx
@@ -0,0 +1,26 @@
+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..cdedc478
--- /dev/null
+++ b/src/components/shared/TcTableContainer/TcTableRow.spec.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import TcTableRow from './TcTableRow';
+
+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/components/shared/TcTableContainer/TcTableRow.tsx b/src/components/shared/TcTableContainer/TcTableRow.tsx
new file mode 100644
index 00000000..bcadf15e
--- /dev/null
+++ b/src/components/shared/TcTableContainer/TcTableRow.tsx
@@ -0,0 +1,46 @@
+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 };
+}
+
+/**
+ * Component to render a table row with custom rendering options.
+ *
+ * @param {ITcTableRowProps} props - The component props.
+ */
+
+function TcTableRow({
+ rowItem,
+ customRenderers,
+ customTableCellClasses,
+ ...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/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/configs/index.ts b/src/configs/index.ts
index 65054cb1..c75829df 100644
--- a/src/configs/index.ts
+++ b/src/configs/index.ts
@@ -7,8 +7,5 @@ export const conf = {
AMPLITUDEANALYTICS_TOKEN: process.env.NEXT_PUBLIC_AMPLITUDEANALYTICS_TOKEN,
PROPERTY_ID: process.env.NEXT_PUBLIC_TAWK_PROPERTY_ID,
WEIGHT_ID: process.env.NEXT_PUBLIC_TAWK_WEIGHT_ID,
- SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
- SENTRY_ENV: process.env.NEXT_PUBLIC_SENTRY_ENV,
- SENTRY_TOKEN: process.env.SENTRY_TOKEN,
DISCORD_CDN: process.env.NEXT_PUBLIC_DISCORD_CDN,
};
diff --git a/src/context/ChannelContext.tsx b/src/context/ChannelContext.tsx
index a789f5e8..deb54d0f 100644
--- a/src/context/ChannelContext.tsx
+++ b/src/context/ChannelContext.tsx
@@ -6,6 +6,7 @@ export interface SubChannel {
name: string;
parentId: string;
canReadMessageHistoryAndViewChannel: boolean;
+ announcementAccess: boolean;
}
export interface Channel {
@@ -30,7 +31,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 +59,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 +87,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 +105,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/context/TokenContext.tsx b/src/context/TokenContext.tsx
index becf358c..25852924 100644
--- a/src/context/TokenContext.tsx
+++ b/src/context/TokenContext.tsx
@@ -21,7 +21,7 @@ type TokenContextType = {
clearToken: () => void;
};
-const TokenContext = createContext(null);
+export const TokenContext = createContext(null);
type TokenProviderProps = {
children: ReactNode;
diff --git a/src/helpers/helper.ts b/src/helpers/helper.ts
index ee764a08..b7819636 100644
--- a/src/helpers/helper.ts
+++ b/src/helpers/helper.ts
@@ -1,3 +1,4 @@
+import moment from 'moment';
import { SelectedSubChannels } from '../context/ChannelContext';
import { IDecodedToken } from '../utils/interfaces';
import { IUser } from '../utils/types';
@@ -120,3 +121,16 @@ export function hexToRGBA(hex: string, opacity: number): string {
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}
+
+export function validateDateTime(date: Date | null, time: Date | null) {
+ if (date && time) {
+ const selectedDateTime = moment(date).set({
+ hour: time.getHours(),
+ minute: time.getMinutes(),
+ second: 0,
+ });
+
+ return selectedDateTime.isAfter(moment());
+ }
+ return false;
+}
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
new file mode 100644
index 00000000..03448945
--- /dev/null
+++ b/src/pages/announcements/create-new-announcements.tsx
@@ -0,0 +1,287 @@
+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 TcPublicMessageContainer from '../../components/announcements/create/publicMessageContainer/TcPublicMessageContainer';
+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';
+import TcBreadcrumbs from '../../components/shared/TcBreadcrumbs';
+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';
+import SimpleBackdrop from '../../components/global/LoadingBackdrop';
+import { FormControlLabel } from '@mui/material';
+import { MdOutlineAnnouncement } from 'react-icons/md';
+import TcIconContainer from '../../components/announcements/create/TcIconContainer';
+import TcIconWithTooltip from '../../components/shared/TcIconWithTooltip';
+import TcSwitch from '../../components/shared/TcSwitch';
+import TcText from '../../components/shared/TcText';
+
+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 router = useRouter();
+ const { createNewAnnouncements, retrievePlatformById } = useAppStore();
+
+ const { community } = useToken();
+
+ const channelContext = useContext(ChannelContext);
+ const { showMessage } = useSnackbar();
+
+ const { refreshData } = channelContext;
+
+ const [channels, setChannels] = useState([]);
+ const [roles, setRoles] = useState([]);
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [isDateValid, setIsDateValid] = useState(true);
+
+ 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 () => {
+ setLoading(true);
+ try {
+ if (platformId) {
+ await retrievePlatformById(platformId);
+ await refreshData(platformId, 'channel', undefined, undefined, false);
+ }
+ setLoading(false);
+ } catch (error) {
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (!platformId) {
+ return;
+ }
+
+ fetchPlatformChannels();
+ }, [platformId]);
+
+ const handleCreateAnnouncements = async (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: data,
+ };
+
+ try {
+ setLoading(true);
+ const data = await createNewAnnouncements(announcementsPayload);
+ if (data) {
+ showMessage('Announcement created successfully', 'success');
+ router.push('/announcements');
+ }
+ } catch (error) {
+ showMessage('Failed to create announcement', 'error');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
{
+ setScheduledAt(selectedTime);
+ }}
+ isDateValid={isDateValid}
+ setIsDateValid={setIsDateValid}
+ />
+ {
+ if (!platformId) return;
+ setChannels(selectedChannels);
+ setPublicAnnouncements({
+ platformId: platformId,
+ template: message,
+ options: {
+ channelIds: selectedChannels.map(
+ (channel) => channel.id
+ ),
+ },
+ });
+ }}
+ />
+
+
+
+
+
+
+
+ {/* {
+ if (!platformId) return;
+
+ const commonData = {
+ platformId: platformId,
+ template: message,
+ };
+
+ let privateAnnouncementsOptions: {
+ roleIds: string[];
+ userIds: string[];
+ } = {
+ roleIds: [],
+ userIds: [],
+ };
+
+ if (selectedRoles && selectedRoles.length > 0) {
+ setRoles(selectedRoles);
+ privateAnnouncementsOptions.roleIds = selectedRoles.map(
+ (role) => role.roleId.toString()
+ );
+ }
+
+ if (selectedUsers && selectedUsers.length > 0) {
+ setUsers(selectedUsers);
+ privateAnnouncementsOptions.userIds = selectedUsers.map(
+ (user) => user.discordId
+ );
+ }
+
+ if (
+ privateAnnouncementsOptions.roleIds.length > 0 ||
+ privateAnnouncementsOptions.userIds.length > 0
+ ) {
+ const combinedPrivateAnnouncement = {
+ ...commonData,
+ options: privateAnnouncementsOptions,
+ };
+
+ setPrivateAnnouncements([combinedPrivateAnnouncement]);
+ }
+ }}
+ /> */}
+
+
+
router.push('/announcements')}
+ variant="outlined"
+ sx={{
+ maxWidth: {
+ xs: '100%',
+ sm: '8rem',
+ },
+ }}
+ />
+
+ handleCreateAnnouncements(true)}
+ />
+
+ handleCreateAnnouncements(e)
+ }
+ />
+
+
+
+ }
+ />
+
+ >
+ );
+}
+
+CreateNewAnnouncements.pageLayout = defaultLayout;
+
+export default CreateNewAnnouncements;
diff --git a/src/pages/announcements/edit-announcements/index.tsx b/src/pages/announcements/edit-announcements/index.tsx
new file mode 100644
index 00000000..678688fd
--- /dev/null
+++ b/src/pages/announcements/edit-announcements/index.tsx
@@ -0,0 +1,313 @@
+import React, { useContext, useEffect, useMemo, useState } from 'react';
+import { defaultLayout } from '../../../layouts/defaultLayout';
+import SEO from '../../../components/global/SEO';
+import { useRouter } from 'next/router';
+import TcPrivateMessageContainer 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 TcConfirmSchaduledAnnouncementsDialog from '../../../components/announcements/TcConfirmSchaduledAnnouncementsDialog';
+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';
+import SimpleBackdrop from '../../../components/global/LoadingBackdrop';
+import { MdOutlineAnnouncement } from 'react-icons/md';
+import TcIconContainer from '../../../components/announcements/create/TcIconContainer';
+import TcText from '../../../components/shared/TcText';
+
+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 [loading, setLoading] = useState(false);
+ const [isDateValid, setIsDateValid] = useState(true);
+
+ 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 {
+ setLoading(true);
+ 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 {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (!id) return;
+ const fetchAnnouncement = async () => {
+ const data = await retrieveAnnouncementById(id);
+
+ setFetchedAnnouncements(data);
+ setScheduledAt(data.scheduledAt);
+ };
+
+ fetchAnnouncement();
+ }, [id]);
+
+ useEffect(() => {
+ fetchPlatformChannels();
+ }, [fetchedAnnouncements]);
+
+ 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 {
+ setLoading(true);
+ const data = await patchExistingAnnouncement(id, announcementsPayload);
+
+ if (data) {
+ showMessage('Announcement updated successfully', 'success');
+ router.push('/announcements');
+ } else {
+ fetchPlatformChannels();
+ }
+ } catch (error) {
+ showMessage('Failed to create announcement', 'error');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
{
+ setScheduledAt(selectedTime);
+ }}
+ isDateValid={isDateValid}
+ setIsDateValid={setIsDateValid}
+ />
+ {
+ if (!platformId) return;
+ setChannels(selectedChannels);
+ setPublicAnnouncements({
+ platformId: platformId,
+ template: message,
+ options: {
+ channelIds: selectedChannels.map(
+ (channel) => channel.id
+ ),
+ },
+ });
+ }}
+ />
+
+
+
+
+
+
+
+ {/* {
+ if (!platformId) return;
+
+ const commonData = {
+ platformId: platformId,
+ template: message,
+ };
+
+ let privateAnnouncementsOptions: {
+ roleIds: string[];
+ userIds: string[];
+ } = {
+ roleIds: [],
+ userIds: [],
+ };
+
+ if (selectedRoles && selectedRoles.length > 0) {
+ setRoles(selectedRoles);
+ privateAnnouncementsOptions.roleIds = selectedRoles.map(
+ (role) => role.roleId.toString()
+ );
+ }
+
+ if (selectedUsers && selectedUsers.length > 0) {
+ setUsers(selectedUsers);
+ privateAnnouncementsOptions.userIds = selectedUsers.map(
+ (user) => user.discordId
+ );
+ }
+
+ if (
+ privateAnnouncementsOptions.roleIds.length > 0 ||
+ privateAnnouncementsOptions.userIds.length > 0
+ ) {
+ const combinedPrivateAnnouncement = {
+ ...commonData,
+ options: privateAnnouncementsOptions,
+ };
+
+ setPrivateAnnouncements([combinedPrivateAnnouncement]);
+ }
+ }}
+ /> */}
+
+
+ handleEditAnnouncements(e)}
+ />
+
+
+ }
+ />
+
+ >
+ );
+}
+
+Index.pageLayout = defaultLayout;
+
+export default Index;
diff --git a/src/pages/announcements/index.tsx b/src/pages/announcements/index.tsx
new file mode 100644
index 00000000..2e8c06c6
--- /dev/null
+++ b/src/pages/announcements/index.tsx
@@ -0,0 +1,254 @@
+import React, { useEffect, useState } 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 router from 'next/router';
+import TcPagination from '../../components/shared/TcPagination';
+import TcTimeZone from '../../components/announcements/TcTimeZone';
+import moment from 'moment';
+import { MdCalendarMonth } from 'react-icons/md';
+import useAppStore from '../../store/useStore';
+import { StorageService } from '../../services/StorageService';
+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';
+import SimpleBackdrop from '../../components/global/LoadingBackdrop';
+
+function Index() {
+ const { retrieveAnnouncements, retrievePlatformById } = useAppStore();
+
+ const { community } = useToken();
+
+ const [loading, setLoading] = useState(false);
+ const [isFirstLoad, setIsFirstLoad] = useState(true);
+ const communityId =
+ StorageService.readLocalStorage('community')?.id;
+
+ const [anchorEl, setAnchorEl] = useState(null);
+ const [selectedDate, setSelectedDate] = useState(null);
+ const [selectedZone, setSelectedZone] = useState(moment.tz.guess());
+ const [dateTimeDisplay, setDateTimeDisplay] = useState('Filter Date');
+
+ 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);
+ if (isFirstLoad) setIsFirstLoad(false);
+ }
+ }
+ };
+
+ useEffect(() => {
+ fetchPlatform();
+ }, [platformId]);
+
+ const [fetchedAnnouncements, setFetchedAnnouncements] = useState(
+ {
+ limit: 8,
+ page: page,
+ results: [],
+ totalPages: 0,
+ totalResults: 0,
+ }
+ );
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ 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);
+ setPage(1);
+ const fullDateTime = moment(date);
+ setDateTimeDisplay(fullDateTime.format('D MMMM YYYY'));
+
+ setAnchorEl(null);
+ }
+ };
+
+ const resetDateFilter = () => {
+ setSelectedDate(null);
+ setDateTimeDisplay('Filter Date');
+
+ setAnchorEl(null);
+ };
+
+ 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(() => {
+ fetchData(selectedDate, selectedZone);
+ }, [selectedZone, selectedDate, page]);
+
+ const handlePageChange = (selectedPage: number) => {
+ setPage(selectedPage);
+ };
+
+ if (isFirstLoad && loading) {
+ return ;
+ }
+
+ return (
+ <>
+
+ {!announcementsPermissions && }
+
+
+
+
+
+ }
+ variant="outlined"
+ onClick={() =>
+ router.push('/announcements/create-new-announcements')
+ }
+ />
+
+
+ }
+ onClick={handleClick}
+ text={dateTimeDisplay}
+ aria-describedby={id}
+ />
+
+
+
+ {fetchedAnnouncements.results.length > 0 ? (
+
+
+
+ ) : (
+
+
+
+
+ )}
+
+
+ {fetchedAnnouncements.totalResults > 8 && (
+
+
+
+ )}
+
+
+ }
+ />
+
+ >
+ );
+}
+
+Index.pageLayout = defaultLayout;
+
+export default Index;
diff --git a/src/pages/callback.tsx b/src/pages/callback.tsx
index 71f5617c..7ce0c5f1 100644
--- a/src/pages/callback.tsx
+++ b/src/pages/callback.tsx
@@ -137,6 +137,14 @@ function Callback() {
setMessage('Discord Authorization during setup on setting faield.');
router.push('/community-settings');
+ case StatusCode.ANNOUNCEMENTS_PERMISSION_FAILURE:
+ setMessage('Announcements grant write permissions faield.');
+ router.push('/announcements');
+
+ case StatusCode.ANNOUNCEMENTS_PERMISSION_SUCCESS:
+ setMessage('Announcements grant write permissions success.');
+ router.push('/announcements');
+
default:
console.error('Unexpected status code received:', code);
setMessage('An unexpected error occurred. Please try again later.');
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/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 716703ef..f1f6e5a3 100644
--- a/src/pages/community-settings/platform/index.tsx
+++ b/src/pages/community-settings/platform/index.tsx
@@ -2,23 +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 6bcb0683..a5613a93 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';
@@ -9,21 +7,11 @@ 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 {
- 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 (
<>
@@ -36,20 +24,18 @@ function Dashboard(): JSX.Element {
return (
<>
-
-
-
-
- Community Insights
-
-
+
+
+
+ Community Insights
+
+
-
+
>
);
}
diff --git a/src/pages/statistics.tsx b/src/pages/statistics.tsx
index 45378585..f5c60d2a 100644
--- a/src/pages/statistics.tsx
+++ b/src/pages/statistics.tsx
@@ -19,13 +19,26 @@ import { useToken } from '../context/TokenContext';
import EmptyState from '../components/global/EmptyState';
import emptyState from '../assets/svg/empty-state.svg';
import Image from 'next/image';
+import { useRouter } from 'next/router';
const Statistics = () => {
const { community } = useToken();
+ const router = useRouter();
+
const platformId = community?.platforms.find(
(platform) => platform.disconnectedAt === null
)?.id;
+ const tabMap: { [key: string]: string } = {
+ activeMembers: '1',
+ disengagedMembers: '2',
+ };
+
+ const reverseTabMap: { [key: string]: string } = {
+ '1': 'activeMembers',
+ '2': 'disengagedMembers',
+ };
+
const [loading, setLoading] = useState
(true);
const [activeMemberDate, setActiveMemberDate] = useState(1);
const [onBoardingMemberDate, setOnBoardingMemberDate] = useState(1);
@@ -40,13 +53,36 @@ const Statistics = () => {
fetchOnboardingMembers,
} = useAppStore();
- const [activeTab, setActiveTab] = useState('1');
+ const [activeTab, setActiveTab] = useState(
+ tabMap[router.query.tab as string] || '1'
+ );
+
+ useEffect(() => {
+ if (!router.isReady) return;
+
+ const handleRouteChange = () => {
+ setActiveTab(tabMap[router.query.tab as string] || '1');
+ };
+
+ handleRouteChange();
+
+ router.events.on('routeChangeComplete', handleRouteChange);
+
+ return () => {
+ router.events.off('routeChangeComplete', handleRouteChange);
+ };
+ }, [router.isReady, router.query.tab]);
const handleTabChange = (
event: React.SyntheticEvent,
newValue: string
): void => {
- setActiveTab(newValue);
+ if (newValue in reverseTabMap) {
+ const urlTabIdentifier = reverseTabMap[newValue];
+ router.push(`/statistics?tab=${urlTabIdentifier}`, undefined, {
+ shallow: true,
+ });
+ }
};
useEffect(() => {
diff --git a/src/store/slices/announcementsSlice.ts b/src/store/slices/announcementsSlice.ts
new file mode 100644
index 00000000..984afcd4
--- /dev/null
+++ b/src/store/slices/announcementsSlice.ts
@@ -0,0 +1,83 @@
+import { StateCreator } from 'zustand';
+import { axiosInstance } from '../../axiosInstance';
+import IAnnouncements, {
+ IRetrieveAnnouncementsProps,
+} from '../types/IAnnouncements';
+import { CreateAnnouncementsPayload } from '../../pages/announcements/create-new-announcements';
+
+const createAnnouncementsSlice: StateCreator = (set, get) => ({
+ retrieveAnnouncements: async ({
+ page,
+ limit,
+ sortBy,
+ timeZone,
+ startDate,
+ endDate,
+ community,
+ }: IRetrieveAnnouncementsProps) => {
+ try {
+ const params = {
+ page,
+ limit,
+ sortBy,
+ ...(timeZone ? { timeZone } : {}),
+ ...(startDate ? { startDate } : {}),
+ ...(endDate ? { endDate } : {}),
+ };
+
+ const { data } = await axiosInstance.get(
+ `/announcements/?communityId=${community}`,
+ { params }
+ );
+
+ 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 (
+ announcementPayload: CreateAnnouncementsPayload
+ ) => {
+ try {
+ const { data } = await axiosInstance.post(
+ `/announcements/`,
+ announcementPayload
+ );
+ return data;
+ } catch (error) {
+ console.error('Failed to create announcements:', error);
+ }
+ },
+ patchExistingAnnouncement: async (
+ id: string,
+ announcementPayload: CreateAnnouncementsPayload
+ ) => {
+ try {
+ const { data } = await axiosInstance.patch(
+ `/announcements/${id}`,
+ announcementPayload
+ );
+ return data;
+ } catch (error) {
+ console.error('Failed to patch announcements:', error);
+ }
+ },
+ deleteAnnouncements: async (id: string) => {
+ try {
+ const { data } = await axiosInstance.delete(`/announcements/${id}`);
+ return data;
+ } catch (error) {
+ console.error('Failed to delete announcements:', error);
+ }
+ },
+});
+
+export default createAnnouncementsSlice;
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/platformSlice.ts b/src/store/slices/platformSlice.ts
index 1d20b05a..8bee293b 100644
--- a/src/store/slices/platformSlice.ts
+++ b/src/store/slices/platformSlice.ts
@@ -5,6 +5,7 @@ import IPlatfrom, {
IRetrievePlatformsProps,
IRetrivePlatformRolesOrChannels,
IPatchPlatformInput,
+ IGrantWritePermissionsProps,
} from '../types/IPlatform';
import { conf } from '../../configs';
import { IPlatformProps } from '../../utils/interfaces';
@@ -71,6 +72,7 @@ const createPlatfromSlice: StateCreator = (set, get) => ({
platformId,
property = 'channel',
name,
+ ngu,
sortBy,
page,
limit,
@@ -88,6 +90,8 @@ const createPlatfromSlice: StateCreator = (set, get) => ({
if (name) params.append('name', name);
+ if (ngu) params.append('ngu', ngu);
+
if (page !== undefined) {
params.append('page', page.toString());
}
@@ -108,6 +112,15 @@ const createPlatfromSlice: StateCreator = (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/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/IAnnouncements.ts b/src/store/types/IAnnouncements.ts
new file mode 100644
index 00000000..672897f2
--- /dev/null
+++ b/src/store/types/IAnnouncements.ts
@@ -0,0 +1,18 @@
+export interface IRetrieveAnnouncementsProps {
+ page: number;
+ limit: number;
+ sortBy?: string;
+ community: string;
+ startDate?: string;
+ endDate?: string;
+ timeZone: string;
+}
+
+export default interface IAnnouncements {
+ retrieveAnnouncements: ({
+ page,
+ limit,
+ sortBy,
+ community,
+ }: IRetrieveAnnouncementsProps) => void;
+}
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/IPlatform.ts b/src/store/types/IPlatform.ts
index 6ad1e5f5..16c7574d 100644
--- a/src/store/types/IPlatform.ts
+++ b/src/store/types/IPlatform.ts
@@ -13,8 +13,9 @@ export interface IRetrivePlatformRolesOrChannels {
limit?: number;
sortBy?: string;
name?: string;
+ ngu?: string;
platformId: string;
- property: 'channel' | 'role';
+ property: 'channel' | 'role' | 'guildMember';
}
export interface IDeletePlatformProps {
@@ -31,6 +32,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 +63,9 @@ export default interface IPlatfrom {
page,
limit,
}: IRetrivePlatformRolesOrChannels) => void;
+ grantWritePermissions: ({
+ platformType,
+ moduleType,
+ id,
+ }: IGrantWritePermissionsProps) => void;
}
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..4a6fd939 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';
@@ -9,11 +7,10 @@ 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) => ({
- ...createAuthSlice(...a),
...createChartSlice(...a),
- ...createSettingSlice(...a),
...createBreakdownsSlice(...a),
...createMemberInteractionSlice(...a),
...communityHealthSlice(...a),
@@ -21,6 +18,7 @@ const useAppStore = create()((...a) => ({
...centricSlice(...a),
...platformSlice(...a),
...userSlice(...a),
+ ...announcementsSlice(...a),
}));
export default useAppStore;
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;
+}
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 d8218423..f34013e6 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 {
@@ -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;
};
@@ -171,3 +193,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;
+}
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');
diff --git a/src/utils/theme.ts b/src/utils/theme.ts
index 38e0d8b1..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: {},
},
},
});
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 = {
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"]
}