diff --git a/bottombar.tsx b/bottombar.tsx
index 323559a..423d7be 100644
--- a/bottombar.tsx
+++ b/bottombar.tsx
@@ -1,7 +1,13 @@
-import { ToolbarPreset, ToolbarComponents } from "customization-api";
+import {
+ ToolbarPreset,
+ ToolbarComponents,
+ useSidePanel,
+} from "customization-api";
import React from "react";
+import { PollButtonWithSidePanel, POLL_SIDEBAR_NAME } from "./polling-ui";
const Bottombar = () => {
+ const { setSidePanel } = useSidePanel();
const {
MeetingTitleToolbarItem,
ParticipantCountToolbarItem,
@@ -62,6 +68,15 @@ const Bottombar = () => {
return w > 767 ? true : false;
},
},
+ poll: {
+ hide: (w) => {
+ return w > 767 ? true : false;
+ },
+ component: PollButtonWithSidePanel,
+ onPress: () => {
+ setSidePanel(POLL_SIDEBAR_NAME);
+ },
+ },
},
},
"meeting-title": {
diff --git a/index.tsx b/index.tsx
index 708c881..d247102 100644
--- a/index.tsx
+++ b/index.tsx
@@ -1,9 +1,23 @@
import { customize, isMobileUA } from "customization-api";
import Topbar from "./topbar";
import Bottombar from "./bottombar";
+import PollSidebar from "./polling/components/PollSidebar";
+import Poll from "./polling/components/Poll";
+import { POLL_SIDEBAR_NAME } from "./polling-ui";
const config = customize({
components: {
+ wrapper: Poll,
+ customSidePanel: () => {
+ return [
+ {
+ name: POLL_SIDEBAR_NAME,
+ component: PollSidebar,
+ title: "Polls",
+ onClose: () => {},
+ },
+ ];
+ },
videoCall: {
topToolBar: Topbar,
bottomToolBar: Bottombar,
diff --git a/polling-ui.tsx b/polling-ui.tsx
new file mode 100644
index 0000000..50ddd2f
--- /dev/null
+++ b/polling-ui.tsx
@@ -0,0 +1,84 @@
+import React from "react";
+import {
+ ToolbarPreset,
+ useSidePanel,
+ ToolbarItem,
+ ImageIcon,
+ ThemeConfig,
+ $config,
+ useActionSheet,
+ IconButton,
+ IconButtonProps,
+} from "customization-api";
+import { View, Text, StyleSheet } from "react-native";
+import pollIcons from "./polling/poll-icons";
+
+const POLL_SIDEBAR_NAME = "side-panel-poll";
+
+const PollButtonWithSidePanel = () => {
+ const { isOnActionSheet } = useActionSheet();
+ const { setSidePanel } = useSidePanel();
+
+ // On smaller screens
+ if (isOnActionSheet) {
+ const iconButtonProps: IconButtonProps = {
+ onPress: () => {
+ setSidePanel(POLL_SIDEBAR_NAME);
+ },
+ iconProps: {
+ icon: pollIcons["bar-chart"],
+ tintColor: $config.SECONDARY_ACTION_COLOR,
+ },
+ btnTextProps: {
+ text: "Polls",
+ textColor: $config.FONT_COLOR,
+ numberOfLines: 1,
+ textStyle: {
+ marginTop: 8,
+ },
+ },
+ isOnActionSheet: isOnActionSheet,
+ };
+
+ return (
+
+
+
+ );
+ }
+ // On bigger screens
+ return (
+
+
+
+
+ Polls
+
+ );
+};
+
+export { PollButtonWithSidePanel, POLL_SIDEBAR_NAME };
+
+const style = StyleSheet.create({
+ toolbarItem: {
+ display: "flex",
+ flexDirection: "row",
+ },
+ toolbarImg: {
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ marginRight: 8,
+ },
+ toolbarText: {
+ color: $config.SECONDARY_ACTION_COLOR,
+ fontSize: ThemeConfig.FontSize.normal,
+ fontWeight: "400",
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ },
+});
diff --git a/polling/components/Poll.tsx b/polling/components/Poll.tsx
new file mode 100644
index 0000000..b5133ee
--- /dev/null
+++ b/polling/components/Poll.tsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import {PollModalType, PollProvider, usePoll} from '../context/poll-context';
+import PollFormWizardModal from './modals/PollFormWizardModal';
+import {PollEventsProvider, PollEventsSubscriber} from '../context/poll-events';
+import PollResponseFormModal from './modals/PollResponseFormModal';
+import PollResultModal from './modals/PollResultModal';
+import PollConfirmModal from './modals/PollEndConfirmModal';
+import PollItemNotFound from './modals/PollItemNotFound';
+import {log} from '../helpers';
+
+function Poll({children}: {children?: React.ReactNode}) {
+ return (
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+function PollModals() {
+ const {modalState, polls} = usePoll();
+ // Log only in development mode to prevent performance hits
+ if (process.env.NODE_ENV === 'development') {
+ log('polls data changed: ', polls);
+ }
+
+ const renderModal = () => {
+ switch (modalState.modalType) {
+ case PollModalType.DRAFT_POLL:
+ if (modalState.id && polls[modalState.id]) {
+ const editFormObject = {...polls[modalState.id]};
+ return ;
+ }
+ return ;
+ case PollModalType.PREVIEW_POLL:
+ if (modalState.id && polls[modalState.id]) {
+ const previewFormObject = {...polls[modalState.id]};
+ return (
+
+ );
+ }
+ break;
+ case PollModalType.RESPOND_TO_POLL:
+ if (modalState.id && polls[modalState.id]) {
+ return ;
+ }
+ return ;
+ case PollModalType.VIEW_POLL_RESULTS:
+ if (modalState.id && polls[modalState.id]) {
+ return ;
+ }
+ return ;
+ case PollModalType.END_POLL_CONFIRMATION:
+ if (modalState.id && polls[modalState.id]) {
+ return ;
+ }
+ return ;
+ case PollModalType.DELETE_POLL_CONFIRMATION:
+ if (modalState.id && polls[modalState.id]) {
+ return (
+
+ );
+ }
+ return ;
+ case PollModalType.NONE:
+ break;
+ default:
+ log('Unknown modal type: ', modalState);
+ return <>>;
+ }
+ };
+
+ return <>{renderModal()}>;
+}
+
+export default Poll;
diff --git a/polling/components/PollAvatarHeader.tsx b/polling/components/PollAvatarHeader.tsx
new file mode 100644
index 0000000..d584f05
--- /dev/null
+++ b/polling/components/PollAvatarHeader.tsx
@@ -0,0 +1,86 @@
+import {Text, View, StyleSheet} from 'react-native';
+import React from 'react';
+import {PollItem} from '../context/poll-context';
+import {
+ useContent,
+ UserAvatar,
+ ThemeConfig,
+ useString,
+ videoRoomUserFallbackText,
+ UidType,
+ $config,
+} from 'customization-api';
+
+interface Props {
+ pollItem: PollItem;
+}
+
+function PollAvatarHeader({pollItem}: Props) {
+ const remoteUserDefaultLabel = useString(videoRoomUserFallbackText)();
+ const {defaultContent} = useContent();
+
+ const getPollCreaterName = ({uid, name}: {uid: UidType; name: string}) => {
+ return defaultContent[uid]?.name || name || remoteUserDefaultLabel;
+ };
+
+ return (
+
+
+
+
+
+
+ {getPollCreaterName(pollItem.createdBy)}
+
+ {pollItem.type}
+
+
+ );
+}
+export const style = StyleSheet.create({
+ titleCard: {
+ display: 'flex',
+ flexDirection: 'row',
+ gap: 12,
+ },
+ title: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 2,
+ },
+ titleAvatar: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ titleAvatarContainer: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ backgroundColor: $config.VIDEO_AUDIO_TILE_AVATAR_COLOR,
+ },
+ titleAvatarContainerText: {
+ fontSize: ThemeConfig.FontSize.small,
+ lineHeight: 16,
+ fontWeight: '600',
+ color: $config.VIDEO_AUDIO_TILE_COLOR,
+ },
+ titleText: {
+ color: $config.FONT_COLOR,
+ fontSize: ThemeConfig.FontSize.normal,
+ fontWeight: '700',
+ lineHeight: 20,
+ },
+ titleSubtext: {
+ color: $config.FONT_COLOR,
+ fontSize: ThemeConfig.FontSize.tiny,
+ fontWeight: '400',
+ lineHeight: 16,
+ },
+});
+
+export default PollAvatarHeader;
diff --git a/polling/components/PollCard.tsx b/polling/components/PollCard.tsx
new file mode 100644
index 0000000..51fb2f2
--- /dev/null
+++ b/polling/components/PollCard.tsx
@@ -0,0 +1,309 @@
+import React from 'react';
+import {Text, View, StyleSheet, TouchableOpacity} from 'react-native';
+import {
+ PollItem,
+ PollStatus,
+ PollTaskRequestTypes,
+ usePoll,
+} from '../context/poll-context';
+import {
+ ThemeConfig,
+ TertiaryButton,
+ useLocalUid,
+ $config,
+ LinkButton,
+ ImageIcon,
+} from 'customization-api';
+import {BaseMoreButton} from '../ui/BaseMoreButton';
+import {PollCardMoreActions} from './PollCardMoreActions';
+import {capitalizeFirstLetter, getPollTypeDesc, hasUserVoted} from '../helpers';
+import {
+ PollFormSubmitButton,
+ PollRenderResponseFormBody,
+} from './form/poll-response-forms';
+import {usePollPermissions} from '../hook/usePollPermissions';
+import {usePollForm} from '../hook/usePollForm';
+
+const PollCardHeader = ({pollItem}: {pollItem: PollItem}) => {
+ const moreBtnRef = React.useRef(null);
+ const [actionMenuVisible, setActionMenuVisible] =
+ React.useState(false);
+ const {editPollForm, handlePollTaskRequest} = usePoll();
+ const {canEdit} = usePollPermissions({pollItem});
+
+ return (
+
+
+
+ {getPollTypeDesc(pollItem.type, pollItem.multiple_response)}
+
+ {pollItem.status === PollStatus.LATER && (
+ <>
+
+ Draft
+ >
+ )}
+
+ {canEdit && (
+
+ {pollItem.status === PollStatus.LATER && (
+ {
+ editPollForm(pollItem.id);
+ }}>
+
+
+ Edit
+
+
+ )}
+
+ {
+ handlePollTaskRequest(action, pollItem.id);
+ setActionMenuVisible(false);
+ }}
+ />
+
+ )}
+
+ );
+};
+
+const PollCardContent = ({pollItem}: {pollItem: PollItem}) => {
+ const {sendResponseToPoll} = usePoll();
+ const {canViewPollDetails} = usePollPermissions({pollItem});
+ const localUid = useLocalUid();
+ const hasSubmittedResponse = hasUserVoted(pollItem.options, localUid);
+
+ const onFormSubmit = (responses: string | string[]) => {
+ sendResponseToPoll(pollItem, responses);
+ };
+
+ const onFormSubmitComplete = () => {
+ // console.log('supriya');
+ // Declaring this method just to have buttonVisible working
+ };
+
+ const {
+ onSubmit,
+ selectedOption,
+ handleRadioSelect,
+ selectedOptions,
+ handleCheckboxToggle,
+ answer,
+ setAnswer,
+ buttonText,
+ submitDisabled,
+ buttonStatus,
+ buttonVisible,
+ } = usePollForm({
+ pollItem,
+ initialSubmitted: hasSubmittedResponse,
+ onFormSubmit,
+ onFormSubmitComplete,
+ });
+
+ return (
+
+
+ {capitalizeFirstLetter(pollItem.question)}
+
+ {pollItem.status === PollStatus.LATER ? (
+ <>>
+ ) : (
+ <>
+
+ {(hasSubmittedResponse && !buttonVisible) ||
+ pollItem.status === PollStatus.FINISHED ? (
+ <>>
+ ) : (
+
+
+
+ )}
+ >
+ )}
+
+ );
+};
+
+const PollCardFooter = ({pollItem}: {pollItem: PollItem}) => {
+ const {handlePollTaskRequest} = usePoll();
+ const {canEnd, canViewPollDetails} = usePollPermissions({pollItem});
+
+ return (
+
+ {canEnd && pollItem.status === PollStatus.ACTIVE && (
+
+ {
+ handlePollTaskRequest(
+ PollTaskRequestTypes.FINISH_CONFIRMATION,
+ pollItem.id,
+ );
+ }}
+ />
+
+ )}
+ {canViewPollDetails && (
+
+
+
+ handlePollTaskRequest(
+ PollTaskRequestTypes.VIEW_DETAILS,
+ pollItem.id,
+ )
+ }
+ />
+
+
+ )}
+
+ );
+};
+
+function PollCard({pollItem}: {pollItem: PollItem}) {
+ return (
+
+
+
+
+ {pollItem.status !== PollStatus.LATER && (
+ <>
+
+ >
+ )}
+
+
+ );
+}
+export {PollCard};
+
+const style = StyleSheet.create({
+ fullWidth: {
+ alignSelf: 'stretch',
+ },
+ pollItem: {
+ marginVertical: 12,
+ },
+ pollCard: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 8,
+ padding: 12,
+ borderRadius: 12,
+ borderWidth: 2,
+ borderColor: $config.CARD_LAYER_3_COLOR,
+ backgroundColor: $config.CARD_LAYER_1_COLOR,
+ },
+ pollCardHeader: {
+ height: 24,
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ pollCardHeaderText: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low,
+ fontSize: ThemeConfig.FontSize.tiny,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: '600',
+ lineHeight: 12,
+ },
+ pollCardContent: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 12,
+ alignSelf: 'stretch',
+ alignItems: 'flex-start',
+ },
+ pollCardContentQuestionText: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.normal,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: '700',
+ lineHeight: 19,
+ },
+ pollCardFooter: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 8,
+ },
+ linkBtnContainer: {
+ height: 36,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ linkText: {
+ textAlign: 'center',
+ color: $config.PRIMARY_ACTION_BRAND_COLOR,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: '600',
+ lineHeight: 16,
+ },
+ space: {
+ marginHorizontal: 8,
+ },
+ row: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ gap8: {
+ gap: 8,
+ },
+ gap5: {
+ gap: 5,
+ },
+ mr8: {
+ marginRight: 8,
+ },
+ dot: {
+ width: 5,
+ height: 5,
+ borderRadius: 3,
+ backgroundColor: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low,
+ },
+ alignRight: {
+ alignSelf: 'flex-end',
+ },
+});
diff --git a/polling/components/PollCardMoreActions.tsx b/polling/components/PollCardMoreActions.tsx
new file mode 100644
index 0000000..ec244eb
--- /dev/null
+++ b/polling/components/PollCardMoreActions.tsx
@@ -0,0 +1,147 @@
+import React, {Dispatch, SetStateAction} from 'react';
+import {View, useWindowDimensions} from 'react-native';
+import {
+ ActionMenu,
+ ActionMenuItem,
+ calculatePosition,
+ ThemeConfig,
+ $config,
+} from 'customization-api';
+import {PollStatus, PollTaskRequestTypes} from '../context/poll-context';
+
+interface PollCardMoreActionsMenuProps {
+ status: PollStatus;
+ moreBtnRef: React.RefObject;
+ actionMenuVisible: boolean;
+ setActionMenuVisible: Dispatch>;
+ onCardActionSelect: (action: PollTaskRequestTypes) => void;
+}
+const PollCardMoreActions = (props: PollCardMoreActionsMenuProps) => {
+ const {
+ actionMenuVisible,
+ setActionMenuVisible,
+ moreBtnRef,
+ onCardActionSelect,
+ status,
+ } = props;
+ const actionMenuItems: ActionMenuItem[] = [];
+ const [modalPosition, setModalPosition] = React.useState({});
+ const [isPosCalculated, setIsPosCalculated] = React.useState(false);
+ const {width: globalWidth, height: globalHeight} = useWindowDimensions();
+
+ status !== PollStatus.FINISHED &&
+ actionMenuItems.push({
+ icon: 'send',
+ iconColor: $config.SECONDARY_ACTION_COLOR,
+ textColor: $config.FONT_COLOR,
+ title: 'Launch Poll',
+ titleStyle: {
+ fontSize: ThemeConfig.FontSize.small,
+ },
+ disabled: status !== PollStatus.LATER,
+ onPress: () => {
+ onCardActionSelect(PollTaskRequestTypes.SEND);
+ setActionMenuVisible(false);
+ },
+ });
+
+ // status === PollStatus.ACTIVE &&
+ // actionMenuItems.push({
+ // icon: 'share',
+ // iconColor: $config.SECONDARY_ACTION_COLOR,
+ // textColor: $config.FONT_COLOR,
+ // title: 'Publish Result',
+ // titleStyle: {
+ // fontSize: ThemeConfig.FontSize.small,
+ // },
+ // onPress: () => {
+ // onCardActionSelect(PollTaskRequestTypes.PUBLISH);
+ // setActionMenuVisible(false);
+ // },
+ // });
+
+ status !== PollStatus.LATER &&
+ actionMenuItems.push({
+ icon: 'download',
+ iconColor: $config.SECONDARY_ACTION_COLOR,
+ textColor: $config.FONT_COLOR,
+ title: 'Export Resuts',
+ titleStyle: {
+ fontSize: ThemeConfig.FontSize.small,
+ },
+ onPress: () => {
+ onCardActionSelect(PollTaskRequestTypes.EXPORT);
+ setActionMenuVisible(false);
+ },
+ });
+
+ actionMenuItems.push({
+ icon: 'close',
+ iconColor: $config.SECONDARY_ACTION_COLOR,
+ textColor: $config.FONT_COLOR,
+ title: 'End Poll',
+ titleStyle: {
+ fontSize: ThemeConfig.FontSize.small,
+ },
+ disabled: status !== PollStatus.ACTIVE,
+ onPress: () => {
+ onCardActionSelect(PollTaskRequestTypes.FINISH_CONFIRMATION);
+ setActionMenuVisible(false);
+ },
+ });
+
+ actionMenuItems.push({
+ icon: 'delete',
+ iconColor: $config.SEMANTIC_ERROR,
+ textColor: $config.SEMANTIC_ERROR,
+ title: 'Delete Poll',
+ titleStyle: {
+ fontSize: ThemeConfig.FontSize.small,
+ },
+ onPress: () => {
+ onCardActionSelect(PollTaskRequestTypes.DELETE_CONFIRMATION);
+ setActionMenuVisible(false);
+ },
+ });
+
+ React.useEffect(() => {
+ if (actionMenuVisible && moreBtnRef.current) {
+ //getting btnRef x,y
+ moreBtnRef?.current?.measure(
+ (
+ _fx: number,
+ _fy: number,
+ localWidth: number,
+ localHeight: number,
+ px: number,
+ py: number,
+ ) => {
+ const data = calculatePosition({
+ px,
+ py,
+ localWidth,
+ localHeight,
+ globalHeight,
+ globalWidth,
+ });
+ setModalPosition(data);
+ setIsPosCalculated(true);
+ },
+ );
+ }
+ }, [actionMenuVisible, globalWidth, globalHeight, moreBtnRef]);
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export {PollCardMoreActions};
diff --git a/polling/components/PollList.tsx b/polling/components/PollList.tsx
new file mode 100644
index 0000000..5016f96
--- /dev/null
+++ b/polling/components/PollList.tsx
@@ -0,0 +1,94 @@
+import React, {useState, useEffect} from 'react';
+import {View} from 'react-native';
+import {PollCard} from './PollCard';
+import {PollItem, PollStatus, usePoll} from '../context/poll-context';
+import {
+ BaseAccordion,
+ BaseAccordionItem,
+ BaseAccordionHeader,
+ BaseAccordionContent,
+} from '../ui/BaseAccordian';
+import {useLocalUid} from 'customization-api';
+
+type PollsGrouped = Array<{key: string; poll: PollItem}>;
+
+export default function PollList() {
+ const {polls} = usePoll();
+ const localUid = useLocalUid();
+
+ // State to keep track of the currently open accordion
+ const [openAccordion, setOpenAccordion] = useState(null);
+
+ // Group polls by their status
+ const groupedPolls = Object.entries(polls).reduce(
+ (acc, [key, poll]) => {
+ // Check if the poll should be included in the LATER group based on creator
+ if (poll.status === PollStatus.LATER && poll.createdBy.uid !== localUid) {
+ return acc; // Skip this poll if it doesn't match the localUid
+ }
+ // Otherwise, add the poll to the corresponding group
+ acc[poll.status].push({key, poll});
+ return acc;
+ },
+ {
+ [PollStatus.LATER]: [] as PollsGrouped,
+ [PollStatus.ACTIVE]: [] as PollsGrouped,
+ [PollStatus.FINISHED]: [] as PollsGrouped,
+ },
+ );
+
+ // Destructure grouped polls for easy access
+ const {
+ LATER: draftPolls,
+ ACTIVE: activePolls,
+ FINISHED: finishedPolls,
+ } = groupedPolls;
+
+ // Set default open accordion based on priority: Active > Draft > Completed
+ useEffect(() => {
+ if (activePolls.length > 0) {
+ setOpenAccordion('active-accordion');
+ } else if (draftPolls.length > 0) {
+ setOpenAccordion('draft-accordion');
+ } else if (finishedPolls.length > 0) {
+ setOpenAccordion('finished-accordion');
+ }
+ }, [activePolls, draftPolls, finishedPolls]);
+
+ // Function to handle accordion toggling
+ const handleAccordionToggle = (id: string) => {
+ setOpenAccordion(prev => (prev === id ? null : id));
+ };
+
+ // Render a section with its corresponding Accordion
+ const renderPollList = (
+ pollsGrouped: PollsGrouped,
+ title: string,
+ id: string,
+ ) => {
+ return pollsGrouped.length ? (
+
+
+ handleAccordionToggle(id)}
+ />
+
+ {pollsGrouped.map(({key, poll}) => (
+
+ ))}
+
+
+
+ ) : null;
+ };
+
+ return (
+
+ {renderPollList(activePolls, 'Active', 'active-accordion')}
+ {renderPollList(draftPolls, 'Saved as Draft', 'draft-accordion')}
+ {renderPollList(finishedPolls, 'Completed', 'finished-accordion')}
+
+ );
+}
diff --git a/polling/components/PollSidebar.tsx b/polling/components/PollSidebar.tsx
new file mode 100644
index 0000000..a55beed
--- /dev/null
+++ b/polling/components/PollSidebar.tsx
@@ -0,0 +1,128 @@
+import React from 'react';
+import {View, StyleSheet, ScrollView, Text} from 'react-native';
+import {
+ PrimaryButton,
+ ThemeConfig,
+ $config,
+ ImageIcon,
+} from 'customization-api';
+import {usePoll} from '../context/poll-context';
+import PollList from './PollList';
+import pollIcons from '../poll-icons';
+import {isWebOnly} from '../helpers';
+import {usePollPermissions} from '../hook/usePollPermissions';
+
+const PollSidebar = () => {
+ const {startPollForm, isHost, polls} = usePoll();
+ const {canCreate} = usePollPermissions({});
+
+ return (
+
+ {Object.keys(polls).length === 0 ? (
+
+
+ {isHost && (
+
+
+
+ )}
+
+ {isHost
+ ? isWebOnly
+ ? 'Visit our web platform to create and manage polls.'
+ : 'Create a new poll and boost interaction with your audience.'
+ : 'No polls here yet...'}
+
+
+
+ ) : (
+
+
+
+ )}
+ {canCreate ? (
+
+ startPollForm()}
+ text="+ Create Poll"
+ />
+
+ ) : (
+ <>>
+ )}
+
+ );
+};
+
+const style = StyleSheet.create({
+ pollSidebar: {
+ display: 'flex',
+ flex: 1,
+ },
+ pollFooter: {
+ padding: 12,
+ backgroundColor: $config.CARD_LAYER_3_COLOR,
+ },
+ emptyCard: {
+ maxWidth: 220,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 12,
+ },
+ emptyCardIcon: {
+ width: 72,
+ height: 72,
+ borderRadius: 12,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: $config.CARD_LAYER_3_COLOR,
+ },
+ emptyView: {
+ display: 'flex',
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 8,
+ },
+ emptyText: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: '400',
+ lineHeight: 20,
+ textAlign: 'center',
+ },
+ scrollViewContent: {},
+ bodyXSmallText: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.medium,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: '400',
+ lineHeight: 16,
+ },
+ btnContainer: {
+ minWidth: 150,
+ height: 36,
+ borderRadius: 4,
+ paddingVertical: 10,
+ paddingHorizontal: 8,
+ },
+ btnText: {
+ color: $config.PRIMARY_ACTION_TEXT_COLOR,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: '600',
+ textTransform: 'capitalize',
+ },
+});
+
+export default PollSidebar;
diff --git a/polling/components/PollTimer.tsx b/polling/components/PollTimer.tsx
new file mode 100644
index 0000000..1475954
--- /dev/null
+++ b/polling/components/PollTimer.tsx
@@ -0,0 +1,54 @@
+import React, {useEffect, useState} from 'react';
+import {Text, View, StyleSheet} from 'react-native';
+import {useCountdown} from '../hook/useCountdownTimer';
+import {ThemeConfig, $config} from 'customization-api';
+
+interface Props {
+ expiresAt: number;
+ setFreezeForm?: React.Dispatch>;
+}
+
+const padZero = (value: number) => {
+ return value.toString().padStart(2, '0');
+};
+
+export default function PollTimer({expiresAt}: Props) {
+ const [days, hours, minutes, seconds] = useCountdown(expiresAt);
+ const [freeze, setFreeze] = useState(false);
+
+ const getTime = () => {
+ if (days) {
+ return `${padZero(days)} : ${padZero(hours)} : ${padZero(
+ minutes,
+ )} : ${padZero(seconds)}`;
+ }
+ if (hours) {
+ return `${padZero(hours)} : ${padZero(minutes)} : ${padZero(seconds)}`;
+ }
+ if (minutes || seconds) {
+ return `${padZero(minutes)} : ${padZero(seconds)}`;
+ }
+ return '00 : 00';
+ };
+
+ useEffect(() => {
+ if (days + hours + minutes + seconds === 0) {
+ setFreeze(true);
+ }
+ }, [days, hours, minutes, seconds, freeze]);
+
+ return (
+
+ {getTime()}
+
+ );
+}
+
+export const style = StyleSheet.create({
+ timer: {
+ color: $config.SEMANTIC_WARNING,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontSize: 16,
+ lineHeight: 20,
+ },
+});
diff --git a/polling/components/form/DraftPollFormView.tsx b/polling/components/form/DraftPollFormView.tsx
new file mode 100644
index 0000000..a388d8b
--- /dev/null
+++ b/polling/components/form/DraftPollFormView.tsx
@@ -0,0 +1,520 @@
+import {
+ Text,
+ View,
+ StyleSheet,
+ TextInput,
+ TouchableOpacity,
+} from 'react-native';
+import React from 'react';
+import {
+ BaseModalTitle,
+ BaseModalContent,
+ BaseModalActions,
+ BaseModalCloseIcon,
+} from '../../ui/BaseModal';
+import {
+ IconButton,
+ PrimaryButton,
+ ThemeConfig,
+ $config,
+ TertiaryButton,
+ ImageIcon,
+ PlatformWrapper,
+} from 'customization-api';
+import {PollFormErrors, PollItem, PollKind} from '../../context/poll-context';
+import {nanoid} from 'nanoid';
+import BaseButtonWithToggle from '../../ui/BaseButtonWithToggle';
+
+function FormTitle({title}: {title: string}) {
+ return (
+
+ {title}
+
+ );
+}
+interface Props {
+ form: PollItem;
+ setForm: React.Dispatch>;
+ onPreview: () => void;
+ onSave: (launch?: boolean) => void;
+ errors: Partial;
+ onClose: () => void;
+}
+
+// Define the form action types and reducer for state management
+const formReducer = (
+ state: PollItem,
+ action: {type: string; payload?: any},
+) => {
+ switch (action.type) {
+ case 'UPDATE_FIELD':
+ return {...state, [action.payload.field]: action.payload.value};
+ case 'UPDATE_OPTION':
+ return {
+ ...state,
+ options: state.options?.map((option, index) =>
+ index === action.payload.index
+ ? {...option, ...action.payload.option}
+ : option,
+ ),
+ };
+ case 'ADD_OPTION':
+ return {
+ ...state,
+ options: [
+ ...(state.options || []),
+ {text: '', value: '', votes: [], percent: '0'},
+ ],
+ };
+ case 'DELETE_OPTION':
+ return {
+ ...state,
+ options:
+ state.options?.filter((_, index) => index !== action.payload.index) ||
+ [],
+ };
+ default:
+ return state;
+ }
+};
+
+export default function DraftPollFormView({
+ form,
+ setForm,
+ onPreview,
+ errors,
+ onClose,
+ onSave,
+}: Props) {
+ const handleInputChange = (field: string, value: string | boolean) => {
+ setForm({
+ ...form,
+ [field]: value,
+ });
+ };
+
+ const handleCheckboxChange = (field: keyof PollItem, value: boolean) => {
+ if (field === 'anonymous' && value) {
+ setForm({
+ ...form,
+ [field]: value,
+ share_attendee: false,
+ share_host: false,
+ });
+ return;
+ } else if (field === 'share_attendee' || field === 'share_host') {
+ if (value) {
+ setForm({
+ ...form,
+ [field]: value,
+ anonymous: false,
+ });
+ return;
+ }
+ }
+ setForm({
+ ...form,
+ [field]: value,
+ });
+ };
+
+ const updateFormOption = (
+ action: 'update' | 'delete' | 'add',
+ value: string,
+ index: number,
+ ) => {
+ if (action === 'add') {
+ setForm({
+ ...form,
+ options: [
+ ...(form.options || []),
+ {
+ text: '',
+ value: '',
+ votes: [],
+ percent: '0',
+ },
+ ],
+ });
+ }
+ if (action === 'update') {
+ setForm(prevForm => ({
+ ...prevForm,
+ options: prevForm.options?.map((option, i) => {
+ if (i === index) {
+ const text = value;
+ const lowerText = text
+ .replace(/\s+/g, '-')
+ .toLowerCase()
+ .concat('-')
+ .concat(nanoid(2));
+ return {
+ ...option,
+ text: text,
+ value: lowerText,
+ };
+ }
+ return option;
+ }),
+ }));
+ }
+ if (action === 'delete') {
+ setForm({
+ ...form,
+ options: form.options?.filter((option, i) => i !== index) || [],
+ });
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {/* Question section */}
+
+
+
+ {
+ handleInputChange('question', text);
+ }}
+ placeholder="Enter your question here..."
+ placeholderTextColor={
+ $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low
+ }
+ />
+ {errors?.question && (
+ {errors.question.message}
+ )}
+
+
+ {/* MCQ section */}
+ {form.type === PollKind.MCQ ? (
+
+
+
+
+
+ {
+ handleCheckboxChange('multiple_response', value);
+ }}
+ />
+
+
+
+
+ {form.options?.map((option, index) => (
+
+
+
+
+ {
+ updateFormOption('update', text, index);
+ }}
+ placeholder={`Option ${index + 1}`}
+ placeholderTextColor={
+ $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low
+ }
+ />
+ {index > 1 ? (
+
+ {
+ updateFormOption('delete', '', index);
+ }}
+ />
+
+ ) : (
+ <>>
+ )}
+
+ ))}
+ {form.options?.length < 5 ? (
+
+ {(isHovered: boolean) => (
+ {
+ updateFormOption('add', '', -1);
+ }}>
+
+ + Add option
+
+
+ )}
+
+ ) : (
+ <>>
+ )}
+ {errors?.options && (
+ {errors.options.message}
+ )}
+
+
+ ) : (
+ <>>
+ )}
+ {/* Yes / No section */}
+ {form.type === PollKind.YES_NO ? (
+
+
+
+
+
+
+
+ Yes
+
+
+
+
+
+ No
+
+
+
+ ) : (
+ <>>
+ )}
+
+
+
+
+
+ {
+ try {
+ onSave(false);
+ } catch (error) {
+ console.error('Error saving form:', error);
+ }
+ }}
+ />
+
+
+ {
+ try {
+ onPreview();
+ } catch (error) {
+ console.error('Error previewing form:', error);
+ }
+ }}
+ />
+
+
+
+ >
+ );
+}
+
+export const style = StyleSheet.create({
+ pForm: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 24,
+ },
+ pFormSection: {
+ gap: 8,
+ },
+ pFormSettings: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 4,
+ padding: 16,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: $config.CARD_LAYER_2_COLOR,
+ },
+ pFormTitle: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 16,
+ fontWeight: '600',
+ },
+ pFormTitleRow: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ pFormTextarea: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.normal,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 16,
+ fontWeight: '400',
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: $config.INPUT_FIELD_BORDER_COLOR,
+ backgroundColor: $config.INPUT_FIELD_BACKGROUND_COLOR,
+ height: 60,
+ outlineStyle: 'none',
+ padding: 20,
+ },
+ pFormOptionText: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.normal,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 24,
+ fontWeight: '400',
+ },
+ pFormOptionPrefix: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low,
+ paddingRight: 4,
+ },
+ pFormOptionLink: {
+ color: $config.PRIMARY_ACTION_BRAND_COLOR,
+ height: 48,
+ paddingVertical: 12,
+ },
+ pFormOptions: {
+ gap: 8,
+ },
+ pFormInput: {
+ flex: 1,
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.normal,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 24,
+ fontWeight: '400',
+ outlineStyle: 'none',
+ borderColor: $config.INPUT_FIELD_BORDER_COLOR,
+ backgroundColor: $config.INPUT_FIELD_BACKGROUND_COLOR,
+ borderRadius: 8,
+ paddingVertical: 12,
+ height: 48,
+ },
+ pFormSettingsText: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.tiny,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 12,
+ fontWeight: '400',
+ },
+ pFormOptionCard: {
+ display: 'flex',
+ paddingHorizontal: 12,
+ flexDirection: 'row',
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ gap: 4,
+ backgroundColor: $config.INPUT_FIELD_BACKGROUND_COLOR,
+ borderColor: $config.INPUT_FIELD_BORDER_COLOR,
+ borderRadius: 8,
+ borderWidth: 1,
+ },
+ noBorder: {
+ borderColor: 'transparent',
+ },
+ pFormToggle: {
+ display: 'flex',
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: 12,
+ justifyContent: 'space-between',
+ position: 'relative',
+ },
+ verticalPadding: {
+ paddingVertical: 12,
+ },
+ pFormCheckboxContainer: {},
+ previewActions: {
+ flex: 1,
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'flex-end',
+ gap: 16,
+ },
+ btnContainer: {
+ minWidth: 150,
+ height: 36,
+ borderRadius: 4,
+ },
+ btnText: {
+ color: $config.PRIMARY_ACTION_TEXT_COLOR,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: '600',
+ textTransform: 'capitalize',
+ },
+ errorBorder: {
+ borderColor: $config.SEMANTIC_ERROR,
+ },
+ hoverBorder: {
+ borderColor: $config.PRIMARY_ACTION_BRAND_COLOR,
+ },
+ errorText: {
+ color: $config.SEMANTIC_ERROR,
+ fontSize: ThemeConfig.FontSize.tiny,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 12,
+ fontWeight: '400',
+ paddingTop: 8,
+ paddingLeft: 8,
+ },
+ pushRight: {
+ marginLeft: 'auto',
+ },
+});
diff --git a/polling/components/form/PreviewPollFormView.tsx b/polling/components/form/PreviewPollFormView.tsx
new file mode 100644
index 0000000..0aa7b30
--- /dev/null
+++ b/polling/components/form/PreviewPollFormView.tsx
@@ -0,0 +1,212 @@
+import {Text, StyleSheet, View, TouchableOpacity} from 'react-native';
+import React from 'react';
+import {
+ BaseModalTitle,
+ BaseModalContent,
+ BaseModalActions,
+ BaseModalCloseIcon,
+} from '../../ui/BaseModal';
+import {PollItem, PollKind} from '../../context/poll-context';
+import {
+ PrimaryButton,
+ TertiaryButton,
+ ThemeConfig,
+ $config,
+ ImageIcon,
+} from 'customization-api';
+
+interface Props {
+ form: PollItem;
+ onEdit: () => void;
+ onSave: (launch: boolean) => void;
+ onClose: () => void;
+}
+
+export default function PreviewPollFormView({
+ form,
+ onEdit,
+ onSave,
+ onClose,
+}: Props) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+ Here is a preview of the poll you will be sending
+
+
+
+ {
+ try {
+ onEdit();
+ } catch (error) {
+ console.error('Error editing form:', error);
+ }
+ }}>
+
+
+ Edit
+
+
+
+
+
+
+
+ {form.question}
+
+ {form.type === PollKind.MCQ || form.type === PollKind.YES_NO ? (
+
+ {form.options?.map((option, index) => (
+
+ {option.text}
+
+ ))}
+
+ ) : (
+ <>>
+ )}
+
+
+
+
+
+
+
+ {
+ try {
+ onSave(false);
+ } catch (error) {
+ console.error('Error saving form:', error);
+ }
+ }}
+ />
+
+
+ {
+ try {
+ onSave(true);
+ } catch (error) {
+ console.error('Error launching form:', error);
+ }
+ }}
+ />
+
+
+
+ >
+ );
+}
+
+export const style = StyleSheet.create({
+ previewContainer: {
+ // width: 550,
+ },
+ previewInfoContainer: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ padding: 20,
+ },
+ previewInfoText: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.normal,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 20,
+ fontWeight: '400',
+ },
+ editSection: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 5,
+ },
+ editText: {
+ color: $config.PRIMARY_ACTION_BRAND_COLOR,
+ fontSize: ThemeConfig.FontSize.normal,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 20,
+ fontWeight: '600',
+ },
+ previewFormContainer: {
+ backgroundColor: $config.BACKGROUND_COLOR,
+ paddingVertical: 40,
+ paddingHorizontal: 60,
+ },
+ previewFormCard: {
+ display: 'flex',
+ flexDirection: 'column',
+ padding: 20,
+ gap: 12,
+ borderRadius: 20,
+ borderWidth: 1,
+ borderColor: $config.CARD_LAYER_4_COLOR,
+ backgroundColor: $config.CARD_LAYER_2_COLOR,
+ },
+ previewQuestion: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.medium,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 24,
+ fontWeight: '600',
+ fontStyle: 'italic',
+ },
+ previewOptionSection: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 8,
+ },
+ previewOptionCard: {
+ display: 'flex',
+ paddingHorizontal: 16,
+ paddingVertical: 10,
+ borderRadius: 6,
+ backgroundColor: $config.CARD_LAYER_4_COLOR,
+ },
+ previewOptionText: {
+ color: $config.SECONDARY_ACTION_COLOR,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: '400',
+ lineHeight: 20,
+ fontStyle: 'italic',
+ },
+ previewActions: {
+ flex: 1,
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ gap: 16,
+ },
+ btnContainer: {
+ minWidth: 150,
+ height: 36,
+ borderRadius: 4,
+ },
+ btnText: {
+ color: $config.PRIMARY_ACTION_TEXT_COLOR,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: '600',
+ textTransform: 'capitalize',
+ },
+});
diff --git a/polling/components/form/SelectNewPollTypeFormView.tsx b/polling/components/form/SelectNewPollTypeFormView.tsx
new file mode 100644
index 0000000..b00714d
--- /dev/null
+++ b/polling/components/form/SelectNewPollTypeFormView.tsx
@@ -0,0 +1,179 @@
+import {Text, StyleSheet, View, TouchableOpacity} from 'react-native';
+import React from 'react';
+import {
+ BaseModalTitle,
+ BaseModalContent,
+ BaseModalCloseIcon,
+} from '../../ui/BaseModal';
+import {PollKind} from '../../context/poll-context';
+import {
+ ThemeConfig,
+ $config,
+ ImageIcon,
+ hexadecimalTransparency,
+ PlatformWrapper,
+} from 'customization-api';
+import {getPollTypeIcon} from '../../helpers';
+
+interface newPollType {
+ key: PollKind;
+ image: null;
+ title: string;
+ description: string;
+}
+
+const newPollTypeConfig: newPollType[] = [
+ {
+ key: PollKind.YES_NO,
+ image: null,
+ title: 'Yes or No Question',
+ description:
+ 'A straightforward question that requires a simple Yes or No answer.',
+ },
+ {
+ key: PollKind.MCQ,
+ image: null,
+ title: 'Multiple Choice Question',
+ description:
+ 'A question with several predefined answer options, allowing users to select one or more responses.',
+ },
+ // {
+ // key: PollKind.OPEN_ENDED,
+ // image: 'question',
+ // title: 'Open Ended Question',
+ // description:
+ // 'A question that invites users to provide a detailed, free-form response, encouraging more in-depth feedback.',
+ // },
+];
+
+export default function SelectNewPollTypeFormView({
+ setType,
+ onClose,
+}: {
+ setType: React.Dispatch>;
+ onClose: () => void;
+}) {
+ return (
+ <>
+
+
+
+
+
+
+
+ What type of question would you like to ask?
+
+
+
+ {newPollTypeConfig.map((item: newPollType) => (
+
+ {(isHovered: boolean) => {
+ return (
+ {
+ setType(item.key);
+ }}
+ style={[style.card, isHovered ? style.cardHover : {}]}>
+
+
+
+
+
+ {item.title}
+
+
+ {item.description}
+
+
+
+ );
+ }}
+
+ ))}
+
+
+
+ >
+ );
+}
+
+export const style = StyleSheet.create({
+ section: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 20,
+ },
+ sectionHeader: {
+ color: $config.FONT_COLOR,
+ fontSize: ThemeConfig.FontSize.normal,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 20,
+ fontWeight: '400',
+ },
+ pollTypeList: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 12,
+ },
+ card: {
+ padding: 12,
+ flexDirection: 'row',
+ gap: 20,
+ outlineStyle: 'none',
+ alignItems: 'center',
+ borderWidth: 1,
+ borderColor: $config.CARD_LAYER_3_COLOR,
+ borderRadius: 8,
+ width: '100%',
+ },
+ cardHover: {
+ backgroundColor: $config.SEMANTIC_NEUTRAL + hexadecimalTransparency['10%'],
+ },
+ cardImage: {
+ width: 100,
+ height: 60,
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderRadius: 8,
+ backgroundColor: $config.CARD_LAYER_3_COLOR,
+ },
+ cardImageHover: {
+ backgroundColor: $config.CARD_LAYER_4_COLOR,
+ },
+ cardContent: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 4,
+ flexShrink: 1,
+ },
+ cardContentTitle: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 16,
+ fontWeight: '700',
+ },
+ cardContentDesc: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low,
+ fontSize: ThemeConfig.FontSize.tiny,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 16,
+ fontWeight: '400',
+ },
+});
diff --git a/polling/components/form/form-config.ts b/polling/components/form/form-config.ts
new file mode 100644
index 0000000..b91d0c2
--- /dev/null
+++ b/polling/components/form/form-config.ts
@@ -0,0 +1,124 @@
+import {nanoid} from 'nanoid';
+import {PollKind, PollItem, PollStatus} from '../../context/poll-context';
+
+const POLL_DURATION = 600; // takes seconds
+
+const getPollExpiresAtTime = (interval: number): number => {
+ const t = new Date();
+ const expiresAT = t.setSeconds(t.getSeconds() + interval);
+ return expiresAT;
+};
+
+const initPollForm = (
+ kind: PollKind,
+ user: {uid: number; name: string},
+): PollItem => {
+ if (kind === PollKind.OPEN_ENDED) {
+ return {
+ id: nanoid(4),
+ type: PollKind.OPEN_ENDED,
+ status: PollStatus.LATER,
+ question: '',
+ answers: null,
+ options: null,
+ multiple_response: false,
+ share_attendee: true,
+ share_host: true,
+ anonymous: false,
+ duration: false,
+ expiresAt: 0,
+ createdAt: Date.now(),
+ createdBy: {...user},
+ };
+ }
+ if (kind === PollKind.MCQ) {
+ return {
+ id: nanoid(4),
+ type: PollKind.MCQ,
+ status: PollStatus.LATER,
+ question: '',
+ answers: null,
+ options: [
+ {
+ text: '',
+ value: '',
+ votes: [],
+ percent: '0',
+ },
+ {
+ text: '',
+ value: '',
+ votes: [],
+ percent: '0',
+ },
+ {
+ text: '',
+ value: '',
+ votes: [],
+ percent: '0',
+ },
+ ],
+ multiple_response: false,
+ share_attendee: true,
+ share_host: true,
+ anonymous: false,
+ duration: false,
+ expiresAt: 0,
+ createdAt: Date.now(),
+ createdBy: {...user},
+ };
+ }
+ if (kind === PollKind.YES_NO) {
+ return {
+ id: nanoid(4),
+ type: PollKind.YES_NO,
+ status: PollStatus.LATER,
+ question: '',
+ answers: null,
+ options: [
+ {
+ text: 'Yes',
+ value: 'yes',
+ votes: [],
+ percent: '0',
+ },
+ {
+ text: 'No',
+ value: 'no',
+ votes: [],
+ percent: '0',
+ },
+ ],
+ multiple_response: false,
+ share_attendee: true,
+ share_host: true,
+ anonymous: false,
+ duration: false,
+ expiresAt: 0,
+ createdAt: Date.now(),
+ createdBy: {...user},
+ };
+ }
+ // If none of the above conditions are met, throw an error or return a default value
+ throw new Error(`Unknown PollKind: ${kind}`);
+};
+
+const getAttributeLengthInKb = (attribute: string): string => {
+ const b = attribute.length * 2;
+ const kb = (b / 1024).toFixed(2);
+ return kb;
+};
+
+const isAttributeLengthValid = (attribute: string) => {
+ if (getAttributeLengthInKb(attribute) > '8') {
+ return false;
+ }
+ return true;
+};
+
+export {
+ getPollExpiresAtTime,
+ initPollForm,
+ isAttributeLengthValid,
+ POLL_DURATION,
+};
diff --git a/polling/components/form/poll-response-forms.tsx b/polling/components/form/poll-response-forms.tsx
new file mode 100644
index 0000000..8934f89
--- /dev/null
+++ b/polling/components/form/poll-response-forms.tsx
@@ -0,0 +1,390 @@
+import {
+ Text,
+ View,
+ StyleSheet,
+ TextInput,
+ TouchableWithoutFeedback,
+} from 'react-native';
+import React, {useState} from 'react';
+import {PollKind, PollStatus} from '../../context/poll-context';
+import {
+ ImageIcon,
+ Checkbox,
+ PrimaryButton,
+ ThemeConfig,
+ $config,
+ useLocalUid,
+ PlatformWrapper,
+} from 'customization-api';
+import BaseRadioButton from '../../ui/BaseRadioButton';
+import {
+ PollOptionList,
+ PollOptionInputListItem,
+ PollItemFill,
+} from '../poll-option-item-ui';
+import {PollFormButton, PollFormInput} from '../../hook/usePollForm';
+
+function PollResponseFormComplete() {
+ return (
+
+
+
+
+
+ Thank you for your response
+
+
+ );
+}
+
+function PollRenderResponseFormBody(
+ props: PollFormInput & {
+ submitted: boolean;
+ submitting: boolean;
+ },
+): JSX.Element {
+ // Directly use switch case logic inside the render
+ switch (props.pollItem.type) {
+ // case PollKind.OPEN_ENDED:
+ // return (
+ //
+ // );
+ case PollKind.MCQ:
+ case PollKind.YES_NO:
+ return ;
+ default:
+ console.error('Unknown poll type:', props.pollItem.type);
+ return Unknown poll type;
+ }
+}
+
+function PollResponseQuestionForm() {
+ const [answer, setAnswer] = useState('');
+
+ return (
+
+
+
+
+
+ );
+}
+
+function PollResponseMCQForm({
+ pollItem,
+ selectedOptions,
+ submitted,
+ handleCheckboxToggle,
+ selectedOption,
+ handleRadioSelect,
+ submitting,
+}: Partial & {
+ submitted: boolean;
+ submitting: boolean;
+}) {
+ const localUid = useLocalUid();
+ return (
+
+
+ {pollItem.multiple_response
+ ? pollItem.options?.map((option, index) => {
+ const myVote = option.votes.some(item => item.uid === localUid);
+ const checked = selectedOptions.includes(option?.value) || myVote;
+ submitted = submitted || pollItem.status === PollStatus.FINISHED;
+ return (
+
+
+
+ {(isHovered: boolean) => (
+
+ <>
+
+
+ handleCheckboxToggle(option?.value)
+ }
+ />
+ >
+
+ )}
+
+
+
+ );
+ })
+ : pollItem.options?.map((option, index) => {
+ const myVote = option.votes.some(item => item.uid === localUid);
+ const checked = selectedOption === option.value || myVote;
+ submitted = submitted || pollItem.status === PollStatus.FINISHED;
+ return (
+
+
+
+ {(isHovered: boolean) => (
+
+ <>
+
+
+ >
+
+ )}
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+function PollFormSubmitButton({
+ buttonText,
+ submitDisabled,
+ onSubmit,
+ buttonStatus,
+}: Partial) {
+ // Define the styles based on button states
+ const getButtonColor = () => {
+ switch (buttonStatus) {
+ case 'initial':
+ return {backgroundColor: $config.SEMANTIC_NEUTRAL};
+ case 'selected':
+ return {backgroundColor: $config.PRIMARY_ACTION_BRAND_COLOR};
+ case 'submitting':
+ return {
+ backgroundColor: $config.PRIMARY_ACTION_BRAND_COLOR,
+ opacity: 0.7,
+ };
+ case 'submitted':
+ return {backgroundColor: $config.SEMANTIC_SUCCESS};
+ default:
+ return {};
+ }
+ };
+ return (
+ {
+ if (buttonStatus === 'submitted') {
+ return;
+ } else {
+ onSubmit();
+ }
+ }}
+ text={buttonText}
+ />
+ );
+}
+
+export {
+ PollResponseQuestionForm,
+ PollResponseMCQForm,
+ PollResponseFormComplete,
+ PollRenderResponseFormBody,
+ PollFormSubmitButton,
+};
+
+export const style = StyleSheet.create({
+ optionsForm: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 20,
+ width: '100%',
+ },
+ thankyouText: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.medium,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 24,
+ fontWeight: '600',
+ },
+ pFormTextarea: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 16,
+ fontWeight: '400',
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: $config.INPUT_FIELD_BORDER_COLOR,
+ backgroundColor: $config.INPUT_FIELD_BACKGROUND_COLOR,
+ height: 110,
+ outlineStyle: 'none',
+ padding: 16,
+ },
+ responseActions: {
+ flex: 1,
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ btnContainer: {
+ minWidth: 150,
+ height: 36,
+ borderRadius: 4,
+ },
+ submittedBtn: {
+ backgroundColor: $config.SEMANTIC_SUCCESS,
+ cursor: 'default',
+ },
+ btnText: {
+ color: $config.PRIMARY_ACTION_TEXT_COLOR,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: '600',
+ textTransform: 'capitalize',
+ },
+ optionListItem: {
+ display: 'flex',
+ flexDirection: 'row',
+ padding: 12,
+ alignItems: 'center',
+ borderWidth: 1,
+ borderColor: $config.CARD_LAYER_3_COLOR,
+ backgroundColor: $config.CARD_LAYER_3_COLOR,
+ },
+ optionText: {
+ color: $config.FONT_COLOR,
+ fontSize: ThemeConfig.FontSize.normal,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: '400',
+ lineHeight: 24,
+ },
+ pFormInput: {
+ flex: 1,
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 16,
+ fontWeight: '400',
+ outlineStyle: 'none',
+ backgroundColor: $config.INPUT_FIELD_BACKGROUND_COLOR,
+ borderRadius: 9,
+ paddingVertical: 12,
+ },
+ centerAlign: {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 12,
+ },
+ mediumHeight: {
+ height: 272,
+ },
+ checkboxContainer: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 12,
+ width: '100%',
+ },
+ checkBox: {
+ borderColor: $config.FONT_COLOR,
+ },
+ checkboxVoted: {
+ borderColor: $config.PRIMARY_ACTION_BRAND_COLOR,
+ backgroundColor: $config.PRIMARY_ACTION_BRAND_COLOR,
+ },
+ checkboxSubmittedAndVoted: {
+ borderColor: $config.FONT_COLOR,
+ backgroundColor: $config.FONT_COLOR,
+ },
+ checkboxSubmittedAndNotVoted: {
+ borderColor: $config.FONT_COLOR,
+ },
+});
diff --git a/polling/components/modals/PollEndConfirmModal.tsx b/polling/components/modals/PollEndConfirmModal.tsx
new file mode 100644
index 0000000..c3c2914
--- /dev/null
+++ b/polling/components/modals/PollEndConfirmModal.tsx
@@ -0,0 +1,106 @@
+import {Text, StyleSheet, View} from 'react-native';
+import React from 'react';
+import {
+ BaseModal,
+ BaseModalTitle,
+ BaseModalContent,
+ BaseModalCloseIcon,
+ BaseModalActions,
+} from '../../ui/BaseModal';
+import {
+ ThemeConfig,
+ $config,
+ TertiaryButton,
+ PrimaryButton,
+} from 'customization-api';
+import {PollTaskRequestTypes, usePoll} from '../../context/poll-context';
+
+interface PollConfirmModalProps {
+ pollId: string;
+ actionType: 'end' | 'delete'; // Define the type of action (end or delete)
+}
+
+export default function PollConfirmModal({
+ pollId,
+ actionType,
+}: PollConfirmModalProps) {
+ const {handlePollTaskRequest, closeCurrentModal} = usePoll();
+
+ const modalTitle = actionType === 'end' ? 'End Poll?' : 'Delete Poll?';
+ const description =
+ actionType === 'end'
+ ? 'This will stop the poll for everyone in this call.'
+ : 'This will permanently delete the poll and its results. This action cannot be undone.';
+
+ const confirmButtonText =
+ actionType === 'end' ? 'End for all' : 'Delete Poll';
+
+ return (
+
+
+
+
+
+
+ {description}
+
+
+
+
+
+
+
+ {
+ if (actionType === 'delete') {
+ handlePollTaskRequest(PollTaskRequestTypes.DELETE, pollId);
+ }
+ if (actionType === 'end') {
+ handlePollTaskRequest(PollTaskRequestTypes.FINISH, pollId);
+ }
+ }}
+ />
+
+
+
+ );
+}
+
+export const style = StyleSheet.create({
+ section: {
+ padding: 20,
+ paddingBottom: 60,
+ },
+ descriptionText: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.normal,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 20,
+ fontWeight: '400',
+ },
+ btnContainer: {
+ minWidth: 150,
+ height: 36,
+ borderRadius: 4,
+ },
+ btnText: {
+ color: $config.PRIMARY_ACTION_TEXT_COLOR,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: '600',
+ textTransform: 'capitalize',
+ },
+ textCenter: {
+ textAlign: 'center',
+ },
+});
diff --git a/polling/components/modals/PollFormWizardModal.tsx b/polling/components/modals/PollFormWizardModal.tsx
new file mode 100644
index 0000000..6691524
--- /dev/null
+++ b/polling/components/modals/PollFormWizardModal.tsx
@@ -0,0 +1,187 @@
+import React, {useEffect, useState, useRef} from 'react';
+import {BaseModal} from '../../ui/BaseModal';
+import SelectNewPollTypeFormView from '../form/SelectNewPollTypeFormView';
+import DraftPollFormView from '../form/DraftPollFormView';
+import PreviewPollFormView from '../form/PreviewPollFormView';
+import {
+ PollItem,
+ PollKind,
+ PollStatus,
+ PollFormErrors,
+} from '../../context/poll-context';
+import {usePoll} from '../../context/poll-context';
+import {initPollForm} from '../form/form-config';
+import {useLocalUid, useContent} from 'customization-api';
+import {log} from '../../helpers';
+
+type FormWizardStep = 'SELECT' | 'DRAFT' | 'PREVIEW';
+
+interface PollFormWizardModalProps {
+ formObject?: PollItem; // Optional prop to initialize form in edit mode
+ formStep?: FormWizardStep;
+}
+
+export default function PollFormWizardModal({
+ formObject,
+ formStep,
+}: PollFormWizardModalProps) {
+ const {savePoll, sendPoll, closeCurrentModal} = usePoll();
+ const [savedPollId, setSavedPollId] = useState(null);
+
+ const [step, setStep] = useState(
+ formObject ? 'DRAFT' : 'SELECT',
+ );
+ const [type, setType] = useState(
+ formObject ? formObject.type : PollKind.NONE,
+ );
+ const [form, setForm] = useState(formObject || null);
+ const [formErrors, setFormErrors] = useState>({});
+
+ const localUid = useLocalUid();
+ const localUidRef = useRef(localUid);
+ const {defaultContent} = useContent();
+ const defaultContentRef = useRef(defaultContent);
+
+ // Monitor savedPollId to send poll when it's updated
+ useEffect(() => {
+ if (savedPollId) {
+ sendPoll(savedPollId);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [savedPollId]);
+
+ useEffect(() => {
+ try {
+ if (formObject) {
+ // If formObject is passed, skip the SELECT step and initialize the form
+ setForm(formObject);
+ if (formStep) {
+ setStep(formStep);
+ } else {
+ setStep('DRAFT');
+ }
+ } else if (type !== PollKind.NONE) {
+ // Initialize the form for a new poll based on the selected type
+ const user = {
+ uid: localUidRef.current,
+ name: defaultContentRef?.current[localUidRef.current]?.name || 'user',
+ };
+ setForm(initPollForm(type, user));
+ setStep('DRAFT');
+ }
+ } catch (error) {
+ log('Error while initializing form: ', error);
+ }
+ }, [type, formObject, formStep]);
+
+ const onSave = (launch?: boolean) => {
+ try {
+ if (!validateForm()) {
+ return;
+ }
+ const payload = {
+ ...form,
+ status: launch ? PollStatus.ACTIVE : PollStatus.LATER,
+ };
+ savePoll(payload);
+ if (launch) {
+ setSavedPollId(payload.id);
+ }
+ } catch (error) {
+ log('error while saving form: ', error);
+ }
+ };
+
+ const onEdit = () => {
+ setStep('DRAFT');
+ };
+
+ const onPreview = () => {
+ if (validateForm()) {
+ setStep('PREVIEW');
+ }
+ };
+
+ const validateForm = () => {
+ // 1. Check if form is null
+ if (!form) {
+ return false;
+ }
+ // 2. Start with an empty errors object
+ let errors: Partial = {};
+
+ // 3. Validate the question field
+ if (form.question.trim() === '') {
+ errors = {
+ ...errors,
+ question: {message: 'This field cannot be empty.'},
+ };
+ }
+
+ // 4. Validate the options for MCQ type poll
+ if (
+ form.type === PollKind.MCQ &&
+ form.options &&
+ (form.options.length === 0 ||
+ form.options.some(item => item.text.trim() === ''))
+ ) {
+ errors = {
+ ...errors,
+ options: {message: 'Option can’t be empty.'},
+ };
+ }
+ // 5. Set formErrors to the collected errors
+ setFormErrors(errors);
+
+ // 6. If there are no errors, return true, otherwise return false
+ return Object.keys(errors).length === 0;
+ };
+
+ const onClose = () => {
+ setFormErrors({});
+ setForm(null);
+ setType(PollKind.NONE);
+ closeCurrentModal();
+ };
+
+ function renderSwitch() {
+ switch (step) {
+ case 'SELECT':
+ return (
+
+ );
+ case 'DRAFT':
+ return (
+ form && (
+
+ )
+ );
+ case 'PREVIEW':
+ return (
+ form && (
+
+ )
+ );
+ default:
+ return <>>;
+ }
+ }
+
+ return (
+
+ {renderSwitch()}
+
+ );
+}
diff --git a/polling/components/modals/PollItemNotFound.tsx b/polling/components/modals/PollItemNotFound.tsx
new file mode 100644
index 0000000..774a00e
--- /dev/null
+++ b/polling/components/modals/PollItemNotFound.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import {Text} from 'react-native';
+import {
+ BaseModal,
+ BaseModalTitle,
+ BaseModalCloseIcon,
+ BaseModalContent,
+} from '../../ui/BaseModal';
+import {usePoll} from '../../context/poll-context';
+import {$config} from 'customization-api';
+
+export default function PollItemNotFound() {
+ const {closeCurrentModal} = usePoll();
+
+ return (
+
+
+
+
+
+
+ This poll has been deleted by the host. Your response was not
+ submitted.
+
+
+
+ );
+}
diff --git a/polling/components/modals/PollResponseFormModal.tsx b/polling/components/modals/PollResponseFormModal.tsx
new file mode 100644
index 0000000..02e0a24
--- /dev/null
+++ b/polling/components/modals/PollResponseFormModal.tsx
@@ -0,0 +1,191 @@
+import React, { useState } from "react";
+import { Text, View, StyleSheet } from "react-native";
+import {
+ BaseModal,
+ BaseModalActions,
+ BaseModalCloseIcon,
+ BaseModalContent,
+ BaseModalTitle,
+} from "../../ui/BaseModal";
+import {
+ PollResponseFormComplete,
+ PollRenderResponseFormBody,
+ PollFormSubmitButton,
+} from "../form/poll-response-forms";
+import {
+ PollStatus,
+ PollTaskRequestTypes,
+ usePoll,
+} from "../../context/poll-context";
+import { getPollTypeDesc } from "../../helpers";
+import {
+ ThemeConfig,
+ $config,
+ TertiaryButton,
+ useSidePanel,
+} from "customization-api";
+import { usePollForm } from "../../hook/usePollForm";
+import { POLL_SIDEBAR_NAME } from "../../../polling-ui";
+
+export default function PollResponseFormModal({ pollId }: { pollId: string }) {
+ const {
+ polls,
+ sendResponseToPoll,
+ closeCurrentModal,
+ handlePollTaskRequest,
+ } = usePoll();
+ const { setSidePanel } = useSidePanel();
+ const [hasResponded, setHasResponded] = useState(false);
+
+ const pollItem = polls[pollId];
+
+ const onFormSubmit = (responses: string | string[]) => {
+ sendResponseToPoll(pollItem, responses);
+ };
+
+ const onFormSubmitComplete = () => {
+ if (pollItem.share_attendee || pollItem.share_host) {
+ handlePollTaskRequest(PollTaskRequestTypes.VIEW_DETAILS, pollItem.id);
+ } else {
+ setHasResponded(true);
+ }
+ };
+
+ const {
+ onSubmit,
+ selectedOption,
+ handleRadioSelect,
+ selectedOptions,
+ handleCheckboxToggle,
+ answer,
+ setAnswer,
+ buttonText,
+ buttonStatus,
+ submitDisabled,
+ } = usePollForm({
+ pollItem,
+ initialSubmitted: false,
+ onFormSubmit,
+ onFormSubmitComplete,
+ });
+
+ const onClose = () => {
+ if (!hasResponded) {
+ setSidePanel(POLL_SIDEBAR_NAME);
+ closeCurrentModal();
+ } else {
+ closeCurrentModal();
+ }
+ };
+
+ return (
+
+
+
+
+
+ {hasResponded ? (
+
+ ) : (
+ <>
+ {pollItem.status === PollStatus.FINISHED && (
+
+
+ This poll has ended. You can no longer submit the response
+
+
+ )}
+
+
+ {getPollTypeDesc(pollItem.type, pollItem.multiple_response)}
+
+ {pollItem.question}
+
+
+ >
+ )}
+
+
+ {hasResponded && (
+
+
+
+ )}
+
+
+
+
+
+ );
+}
+export const style = StyleSheet.create({
+ header: {
+ display: "flex",
+ flexDirection: "column",
+ gap: 8,
+ },
+ heading: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.medium,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 24,
+ fontWeight: "600",
+ },
+ info: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low,
+ fontSize: ThemeConfig.FontSize.tiny,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: "600",
+ lineHeight: 12,
+ },
+ warning: {
+ color: $config.SEMANTIC_ERROR,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: "600",
+ lineHeight: 12,
+ },
+ btnContainer: {
+ minWidth: 150,
+ height: 36,
+ borderRadius: 4,
+ },
+ submittedBtn: {
+ backgroundColor: $config.SEMANTIC_SUCCESS,
+ cursor: "default",
+ },
+ btnText: {
+ color: $config.FONT_COLOR,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: "600",
+ textTransform: "capitalize",
+ },
+});
diff --git a/polling/components/modals/PollResultModal.tsx b/polling/components/modals/PollResultModal.tsx
new file mode 100644
index 0000000..863ce0a
--- /dev/null
+++ b/polling/components/modals/PollResultModal.tsx
@@ -0,0 +1,374 @@
+import {Text, StyleSheet, View} from 'react-native';
+import React from 'react';
+import {
+ BaseModal,
+ BaseModalTitle,
+ BaseModalContent,
+ BaseModalCloseIcon,
+ BaseModalActions,
+} from '../../ui/BaseModal';
+import {
+ ThemeConfig,
+ $config,
+ UserAvatar,
+ TertiaryButton,
+ useContent,
+ useLocalUid,
+ ImageIcon,
+ isMobileUA,
+} from 'customization-api';
+import {
+ PollItemOptionItem,
+ PollTaskRequestTypes,
+ usePoll,
+} from '../../context/poll-context';
+import {
+ formatTimestampToTime,
+ calculateTotalVotes,
+ getPollTypeDesc,
+ capitalizeFirstLetter,
+ getPollTypeIcon,
+} from '../../helpers';
+import {usePollPermissions} from '../../hook/usePollPermissions';
+
+export default function PollResultModal({pollId}: {pollId: string}) {
+ const {polls, closeCurrentModal, handlePollTaskRequest} = usePoll();
+ const localUid = useLocalUid();
+ const {defaultContent} = useContent();
+
+ const pollItem = polls[pollId];
+ const {canViewWhoVoted} = usePollPermissions({pollItem});
+
+ return (
+
+
+
+
+
+
+
+
+
+ {capitalizeFirstLetter(pollItem.question)}
+
+
+ Total Responses {calculateTotalVotes(pollItem.options)}
+
+
+
+
+ Created{' '}
+
+ {formatTimestampToTime(pollItem.createdAt)}{' '}
+
+ by{' '}
+
+ {localUid === pollItem.createdBy.uid
+ ? 'You'
+ : defaultContent[pollItem.createdBy.uid]?.name ||
+ pollItem.createdBy?.name ||
+ 'user'}
+
+
+ {!isMobileUA() && }
+
+
+
+
+
+
+ {getPollTypeDesc(pollItem.type, pollItem.multiple_response)}
+
+
+
+
+
+
+ {pollItem.options?.map((option: PollItemOptionItem, index) => (
+
+
+
+
+ {`Option ${index + 1}`}
+
+
+ {option.text}
+
+
+
+
+ {option.percent}%
+
+
+
+ {option.votes.length} votes
+
+
+
+ {canViewWhoVoted && (
+
+ {option.votes.length > 0 ? (
+ option.votes.map((item, i) => (
+
+
+
+
+ {defaultContent[item.uid]?.name ||
+ item?.name ||
+ 'user'}
+
+
+
+
+ Voted {formatTimestampToTime(item.timestamp)}
+
+
+
+ ))
+ ) : (
+
+ No votes here
+
+ )}
+
+ )}
+
+ ))}
+ {!canViewWhoVoted && (
+
+ Individual responses are anonymous
+
+ )}
+
+
+
+
+
+
+
+
+ {
+ handlePollTaskRequest(PollTaskRequestTypes.EXPORT, pollItem.id);
+ }}
+ />
+
+
+
+ );
+}
+
+export const style = StyleSheet.create({
+ resultContainer: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 16,
+ backgroundColor: $config.BACKGROUND_COLOR,
+ },
+ resultInfoContainer: {
+ paddingVertical: 12,
+ paddingHorizontal: 32,
+ backgroundColor: $config.CARD_LAYER_1_COLOR,
+ display: 'flex',
+ flexDirection: 'column',
+ minHeight: 68,
+ },
+ resultInfoContainerGapWeb: {
+ gap: 4,
+ },
+ resultInfoContainerGapMobile: {
+ gap: 10,
+ },
+ headerWeb: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ gap: 12,
+ },
+ headerMobile: {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'flex-start',
+ justifyContent: 'center',
+ gap: 4,
+ },
+ subheaderWeb: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 12,
+ },
+ subheaderMobile: {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'flex-start',
+ gap: 4,
+ },
+ percentText: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 12,
+ },
+ rowCenter: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ resultSummaryContainer: {
+ paddingVertical: 16,
+ paddingHorizontal: 20,
+ gap: 16,
+ },
+ summaryCard: {
+ display: 'flex',
+ flexDirection: 'column',
+ borderRadius: 8,
+ overflow: 'hidden',
+ borderWidth: 1,
+ borderColor: $config.CARD_LAYER_2_COLOR,
+ },
+ summaryCardHeader: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ backgroundColor: $config.CARD_LAYER_2_COLOR,
+ paddingHorizontal: 20,
+ paddingVertical: 12,
+ },
+ summaryCardBody: {
+ paddingHorizontal: 20,
+ paddingVertical: 12,
+ backgroundColor: $config.CARD_LAYER_1_COLOR,
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 8,
+ },
+ summaryItem: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ titleAvatar: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 8,
+ },
+ titleAvatarContainer: {
+ width: 24,
+ height: 24,
+ borderRadius: 12,
+ backgroundColor: $config.VIDEO_AUDIO_TILE_AVATAR_COLOR,
+ },
+ titleAvatarContainerText: {
+ fontSize: ThemeConfig.FontSize.tiny,
+ lineHeight: 12,
+ fontWeight: '600',
+ color: $config.BACKGROUND_COLOR,
+ },
+ questionText: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.medium,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 24,
+ fontWeight: '600',
+ flexBasis: '75%',
+ },
+ totalText: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.normal,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 20,
+ fontWeight: '400',
+ },
+ descriptionText: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low,
+ fontSize: ThemeConfig.FontSize.small,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 16,
+ fontWeight: '400',
+ fontStyle: 'italic',
+ },
+ bold: {
+ fontWeight: '600',
+ },
+ youText: {
+ color: $config.VIDEO_AUDIO_TILE_AVATAR_COLOR,
+ },
+ light: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low,
+ },
+ smallText: {
+ color: $config.FONT_COLOR,
+ fontSize: ThemeConfig.FontSize.tiny,
+ fontWeight: '400',
+ lineHeight: 16,
+ },
+ username: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.small,
+ fontWeight: '400',
+ lineHeight: 20,
+ },
+ alignRight: {
+ textAlign: 'right',
+ },
+ btnContainer: {
+ minWidth: 150,
+ height: 36,
+ borderRadius: 4,
+ },
+ dot: {
+ width: 5,
+ height: 5,
+ borderRadius: 3,
+ backgroundColor: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low,
+ },
+ textCenter: {
+ textAlign: 'center',
+ },
+ imageIconBox: {
+ width: 15,
+ height: 15,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginRight: 5,
+ },
+});
diff --git a/polling/components/poll-option-item-ui.tsx b/polling/components/poll-option-item-ui.tsx
new file mode 100644
index 0000000..89db63c
--- /dev/null
+++ b/polling/components/poll-option-item-ui.tsx
@@ -0,0 +1,126 @@
+import React from 'react';
+import {Text, View, StyleSheet, DimensionValue} from 'react-native';
+import {ThemeConfig, $config, hexadecimalTransparency} from 'customization-api';
+
+function PollOptionList({children}: {children: React.ReactNode}) {
+ return {children};
+}
+
+interface Props {
+ submitting: boolean;
+ submittedMyVote: boolean;
+ percent: string;
+}
+function PollItemFill({submitting, submittedMyVote, percent}: Props) {
+ return (
+ <>
+
+
+ {`${percent}%`}
+
+ >
+ );
+}
+
+interface PollOptionInputListItem {
+ index: number;
+ checked: boolean;
+ hovered: boolean;
+ children: React.ReactChild;
+}
+
+function PollOptionInputListItem({
+ index,
+ checked,
+ hovered,
+ children,
+}: PollOptionInputListItem) {
+ return (
+
+ {children}
+
+ );
+}
+
+const OPTION_LIST_ITEM_PADDING = 12;
+
+const style = StyleSheet.create({
+ optionsList: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 8,
+ width: '100%',
+ },
+ optionListItem: {
+ display: 'flex',
+ flexDirection: 'row',
+ padding: OPTION_LIST_ITEM_PADDING,
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: $config.CARD_LAYER_3_COLOR,
+ backgroundColor: $config.CARD_LAYER_1_COLOR,
+ overflow: 'hidden',
+ width: '100%',
+ position: 'relative',
+ },
+ optionListItemInput: {
+ backgroundColor: $config.CARD_LAYER_3_COLOR,
+ padding: 0,
+ },
+ optionListItemChecked: {
+ borderColor: $config.PRIMARY_ACTION_BRAND_COLOR,
+ },
+ optionListItemHovered: {
+ borderColor: $config.SEMANTIC_NEUTRAL + hexadecimalTransparency['25%'],
+ },
+ optionFillBackground: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: 0,
+ },
+ optionFillText: {
+ position: 'absolute',
+ top: OPTION_LIST_ITEM_PADDING,
+ bottom: 0,
+ right: OPTION_LIST_ITEM_PADDING,
+ },
+ optionText: {
+ color: $config.FONT_COLOR,
+ fontSize: ThemeConfig.FontSize.normal,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: '700',
+ lineHeight: 24,
+ },
+ myVote: {
+ color: $config.PRIMARY_ACTION_BRAND_COLOR,
+ },
+ pushRight: {
+ marginLeft: 'auto',
+ },
+});
+
+export {PollOptionList, PollOptionInputListItem, PollItemFill};
diff --git a/polling/context/poll-context.tsx b/polling/context/poll-context.tsx
new file mode 100644
index 0000000..ce41794
--- /dev/null
+++ b/polling/context/poll-context.tsx
@@ -0,0 +1,853 @@
+import React, {
+ createContext,
+ useReducer,
+ useEffect,
+ useState,
+ useMemo,
+ useRef,
+ useCallback,
+} from "react";
+import { usePollEvents } from "./poll-events";
+import {
+ useLocalUid,
+ useRoomInfo,
+ useSidePanel,
+ SidePanelType,
+ useContent,
+ isWeb,
+} from "customization-api";
+import {
+ getPollExpiresAtTime,
+ POLL_DURATION,
+} from "../components/form/form-config";
+import {
+ addVote,
+ arrayToCsv,
+ calculatePercentage,
+ debounce,
+ downloadCsv,
+ log,
+ mergePolls,
+} from "../helpers";
+import { POLL_SIDEBAR_NAME } from "../../polling-ui";
+
+enum PollStatus {
+ ACTIVE = "ACTIVE",
+ FINISHED = "FINISHED",
+ LATER = "LATER",
+}
+
+enum PollKind {
+ OPEN_ENDED = "OPEN_ENDED",
+ MCQ = "MCQ",
+ YES_NO = "YES_NO",
+ NONE = "NONE",
+}
+
+enum PollModalType {
+ NONE = "NONE",
+ DRAFT_POLL = "DRAFT_POLL",
+ PREVIEW_POLL = "PREVIEW_POLL",
+ RESPOND_TO_POLL = "RESPOND_TO_POLL",
+ VIEW_POLL_RESULTS = "VIEW_POLL_RESULTS",
+ END_POLL_CONFIRMATION = "END_POLL_CONFIRMATION",
+ DELETE_POLL_CONFIRMATION = "DELETE_POLL_CONFIRMATION",
+}
+
+interface PollModalState {
+ modalType: PollModalType;
+ id: string;
+}
+
+enum PollTaskRequestTypes {
+ SAVE = "SAVE",
+ SEND = "SEND",
+ PUBLISH = "PUBLISH",
+ EXPORT = "EXPORT",
+ VIEW_DETAILS = "VIEW_DETAILS",
+ FINISH = "FINISH",
+ FINISH_CONFIRMATION = "FINISH_CONFIRMATION",
+ DELETE = "DELETE",
+ DELETE_CONFIRMATION = "DELETE_CONFIRMATION",
+ SHARE = "SHARE",
+ SYNC_COMPLETE = "SYNC_COMPLETE",
+}
+
+interface PollItemOptionItem {
+ text: string;
+ value: string;
+ votes: Array<{ uid: number; name: string; timestamp: number }>;
+ percent: string;
+}
+interface PollItem {
+ id: string;
+ type: PollKind;
+ status: PollStatus;
+ question: string;
+ answers: Array<{
+ uid: number;
+ response: string;
+ timestamp: number;
+ }> | null;
+ options: Array | null;
+ multiple_response: boolean;
+ share_attendee: boolean;
+ share_host: boolean;
+ anonymous: boolean;
+ duration: boolean;
+ expiresAt: number;
+ createdBy: { uid: number; name: string };
+ createdAt: number;
+}
+
+type Poll = Record;
+
+interface PollFormErrors {
+ question?: {
+ message: string;
+ };
+ options?: {
+ message: string;
+ };
+}
+
+enum PollActionKind {
+ SAVE_POLL_ITEM = "SAVE_POLL_ITEM",
+ ADD_POLL_ITEM = "ADD_POLL_ITEM",
+ SEND_POLL_ITEM = "SEND_POLL_ITEM",
+ SUBMIT_POLL_ITEM_RESPONSES = "SUBMIT_POLL_ITEM_RESPONSES",
+ RECEIVE_POLL_ITEM_RESPONSES = "RECEIVE_POLL_ITEM_RESPONSES",
+ PUBLISH_POLL_ITEM = "PUBLISH_POLL_ITEM",
+ DELETE_POLL_ITEM = "DELETE_POLL_ITEM",
+ EXPORT_POLL_ITEM = "EXPORT_POLL_ITEM",
+ FINISH_POLL_ITEM = "FINISH_POLL_ITEM",
+ RESET = "RESET",
+ SYNC_COMPLETE = "SYNC_COMPLETE",
+}
+
+type PollAction =
+ | {
+ type: PollActionKind.ADD_POLL_ITEM;
+ payload: {
+ item: PollItem;
+ };
+ }
+ | {
+ type: PollActionKind.SAVE_POLL_ITEM;
+ payload: { item: PollItem };
+ }
+ | {
+ type: PollActionKind.SEND_POLL_ITEM;
+ payload: { pollId: string };
+ }
+ | {
+ type: PollActionKind.SUBMIT_POLL_ITEM_RESPONSES;
+ payload: {
+ id: string;
+ responses: string | string[];
+ user: { name: string; uid: number };
+ timestamp: number;
+ };
+ }
+ | {
+ type: PollActionKind.RECEIVE_POLL_ITEM_RESPONSES;
+ payload: {
+ id: string;
+ responses: string | string[];
+ user: { name: string; uid: number };
+ timestamp: number;
+ };
+ }
+ | {
+ type: PollActionKind.PUBLISH_POLL_ITEM;
+ payload: { pollId: string };
+ }
+ | {
+ type: PollActionKind.FINISH_POLL_ITEM;
+ payload: { pollId: string };
+ }
+ | {
+ type: PollActionKind.EXPORT_POLL_ITEM;
+ payload: { pollId: string };
+ }
+ | {
+ type: PollActionKind.DELETE_POLL_ITEM;
+ payload: { pollId: string };
+ }
+ | {
+ type: PollActionKind.RESET;
+ payload: null;
+ }
+ | {
+ type: PollActionKind.SYNC_COMPLETE;
+ payload: {
+ latestTask: PollTaskRequestTypes;
+ latestPollId: string;
+ };
+ };
+
+function pollReducer(state: Poll, action: PollAction): Poll {
+ switch (action.type) {
+ case PollActionKind.SAVE_POLL_ITEM: {
+ const pollId = action.payload.item.id;
+ return {
+ ...state,
+ [pollId]: { ...action.payload.item },
+ };
+ }
+ case PollActionKind.ADD_POLL_ITEM: {
+ const pollId = action.payload.item.id;
+ return {
+ ...state,
+ [pollId]: { ...action.payload.item },
+ };
+ }
+ case PollActionKind.SEND_POLL_ITEM: {
+ const pollId = action.payload.pollId;
+ return {
+ ...state,
+ [pollId]: {
+ ...state[pollId],
+ status: PollStatus.ACTIVE,
+ expiresAt: getPollExpiresAtTime(POLL_DURATION),
+ },
+ };
+ }
+ case PollActionKind.SUBMIT_POLL_ITEM_RESPONSES: {
+ const { id: pollId, user, responses, timestamp } = action.payload;
+ const poll = state[pollId];
+ if (poll.type === PollKind.OPEN_ENDED && typeof responses === "string") {
+ return {
+ ...state,
+ [pollId]: {
+ ...poll,
+ answers: poll.answers
+ ? [
+ ...poll.answers,
+ {
+ ...user,
+ response: responses,
+ timestamp,
+ },
+ ]
+ : [{ ...user, response: responses, timestamp }],
+ },
+ };
+ }
+ if (
+ (poll.type === PollKind.MCQ || poll.type === PollKind.YES_NO) &&
+ Array.isArray(responses)
+ ) {
+ const newCopyOptions = poll.options?.map((item) => ({ ...item })) || [];
+ const withVotesOptions = addVote(
+ responses,
+ newCopyOptions,
+ user,
+ timestamp
+ );
+ const withPercentOptions = calculatePercentage(withVotesOptions);
+ return {
+ ...state,
+ [pollId]: {
+ ...poll,
+ options: withPercentOptions,
+ },
+ };
+ }
+ return state;
+ }
+ case PollActionKind.RECEIVE_POLL_ITEM_RESPONSES: {
+ const { id: pollId, user, responses, timestamp } = action.payload;
+ const poll = state[pollId];
+ if (poll.type === PollKind.OPEN_ENDED && typeof responses === "string") {
+ return {
+ ...state,
+ [pollId]: {
+ ...poll,
+ answers: poll.answers
+ ? [
+ ...poll.answers,
+ {
+ ...user,
+ response: responses,
+ timestamp,
+ },
+ ]
+ : [{ ...user, response: responses, timestamp }],
+ },
+ };
+ }
+ if (
+ (poll.type === PollKind.MCQ || poll.type === PollKind.YES_NO) &&
+ Array.isArray(responses)
+ ) {
+ const newCopyOptions = poll.options?.map((item) => ({ ...item })) || [];
+ const withVotesOptions = addVote(
+ responses,
+ newCopyOptions,
+ user,
+ timestamp
+ );
+ const withPercentOptions = calculatePercentage(withVotesOptions);
+ return {
+ ...state,
+ [pollId]: {
+ ...poll,
+ options: withPercentOptions,
+ },
+ };
+ }
+ return state;
+ }
+ case PollActionKind.PUBLISH_POLL_ITEM:
+ // No action need just return the state
+ return state;
+ case PollActionKind.FINISH_POLL_ITEM:
+ {
+ const pollId = action.payload.pollId;
+ if (pollId) {
+ return {
+ ...state,
+ [pollId]: { ...state[pollId], status: PollStatus.FINISHED },
+ };
+ }
+ }
+ return state;
+ case PollActionKind.EXPORT_POLL_ITEM:
+ {
+ const pollId = action.payload.pollId;
+ if (pollId && state[pollId]) {
+ const data = state[pollId].options || []; // Provide a fallback in case options is null
+ let csv = arrayToCsv(state[pollId].question, data);
+ downloadCsv(csv, "polls.csv");
+ }
+ }
+ return state;
+ case PollActionKind.DELETE_POLL_ITEM:
+ {
+ const pollId = action.payload.pollId;
+ if (pollId) {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { [pollId]: _, ...newItems } = state;
+ return {
+ ...newItems,
+ };
+ }
+ }
+ return state;
+ case PollActionKind.RESET: {
+ return {};
+ }
+ default: {
+ return state;
+ }
+ }
+}
+
+interface PollContextValue {
+ polls: Poll;
+ startPollForm: () => void;
+ editPollForm: (pollId: string) => void;
+ savePoll: (item: PollItem) => void;
+ sendPoll: (pollId: string) => void;
+ onPollReceived: (
+ polls: Poll,
+ pollId: string,
+ task: PollTaskRequestTypes,
+ isInitialized: boolean
+ ) => void;
+ sendResponseToPoll: (item: PollItem, responses: string | string[]) => void;
+ onPollResponseReceived: (
+ pollId: string,
+ responses: string | string[],
+ user: {
+ uid: number;
+ name: string;
+ },
+ timestamp: number
+ ) => void;
+ sendPollResults: (pollId: string) => void;
+ modalState: PollModalState;
+ closeCurrentModal: () => void;
+ isHost: boolean;
+ handlePollTaskRequest: (task: PollTaskRequestTypes, pollId: string) => void;
+}
+
+const PollContext = createContext(null);
+PollContext.displayName = "PollContext";
+
+function PollProvider({ children }: { children: React.ReactNode }) {
+ const [polls, dispatch] = useReducer(pollReducer, {});
+ const [modalState, setModalState] = useState({
+ modalType: PollModalType.NONE,
+ id: null,
+ });
+ const [lastAction, setLastAction] = useState(null);
+ const { setSidePanel } = useSidePanel();
+ const {
+ data: { isHost },
+ } = useRoomInfo();
+ const localUid = useLocalUid();
+ const { defaultContent } = useContent();
+ const { syncPollEvt, sendResponseToPollEvt } = usePollEvents();
+
+ const callDebouncedSyncPoll = useMemo(
+ () => debounce(syncPollEvt, 800),
+ [syncPollEvt]
+ );
+
+ const pollsRef = useRef(polls);
+
+ useEffect(() => {
+ pollsRef.current = polls; // Update the ref whenever polls changes
+ }, [polls]);
+
+ useEffect(() => {
+ // Delete polls created by the user
+ const deleteMyPolls = () => {
+ Object.values(pollsRef.current).forEach((poll) => {
+ if (poll.createdBy.uid === localUid) {
+ enhancedDispatch({
+ type: PollActionKind.DELETE_POLL_ITEM,
+ payload: { pollId: poll.id },
+ });
+ }
+ });
+ };
+ const handleBeforeUnload = (event: BeforeUnloadEvent) => {
+ event.preventDefault();
+ deleteMyPolls();
+ event.returnValue = ""; // Chrome requires returnValue to be set
+ };
+ if (isWeb()) {
+ window.addEventListener("beforeunload", handleBeforeUnload);
+ }
+
+ return () => {
+ if (isWeb()) {
+ window.removeEventListener("beforeunload", handleBeforeUnload);
+ } else {
+ deleteMyPolls();
+ }
+ };
+ }, [localUid]);
+
+ const enhancedDispatch = (action: PollAction) => {
+ log(`Dispatching action: ${action.type} with payload:`, action.payload);
+ dispatch(action);
+ setLastAction(action);
+ };
+
+ const closeCurrentModal = useCallback(() => {
+ log("Closing current modal.");
+ setModalState({
+ modalType: PollModalType.NONE,
+ id: null,
+ });
+ }, []);
+
+ useEffect(() => {
+ log("useEffect for lastAction triggered", lastAction);
+
+ if (!lastAction) {
+ log("No lastAction to process. Exiting useEffect.");
+ return;
+ }
+ if (!pollsRef?.current) {
+ log("PollsRef.current is undefined or null");
+ return;
+ }
+
+ try {
+ switch (lastAction.type) {
+ case PollActionKind.SAVE_POLL_ITEM:
+ if (lastAction?.payload?.item?.status === PollStatus.LATER) {
+ log("Handling SAVE_POLL_ITEM saving poll item and syncing states");
+ const { item } = lastAction.payload;
+ syncPollEvt(pollsRef.current, item.id, PollTaskRequestTypes.SAVE);
+ closeCurrentModal();
+ }
+ break;
+ case PollActionKind.SEND_POLL_ITEM:
+ {
+ log("Handling SEND_POLL_ITEM");
+ const { pollId } = lastAction.payload;
+ if (pollId && pollsRef.current[pollId]) {
+ syncPollEvt(pollsRef.current, pollId, PollTaskRequestTypes.SEND);
+ closeCurrentModal();
+ } else {
+ log("Invalid pollId or poll not found in state:", pollId);
+ }
+ }
+ break;
+ case PollActionKind.SUBMIT_POLL_ITEM_RESPONSES:
+ log("Handling SUBMIT_POLL_ITEM_RESPONSES");
+ const { id, responses, user, timestamp } = lastAction.payload;
+ if (localUid === pollsRef.current[id]?.createdBy.uid) {
+ log(
+ "No need to send event. User is the poll creator. We only sync data"
+ );
+ syncPollEvt(pollsRef.current, id, PollTaskRequestTypes.SAVE);
+ return;
+ }
+ if (localUid && user?.uid && pollsRef.current[id]) {
+ sendResponseToPollEvt(
+ pollsRef.current[id],
+ responses,
+ user,
+ timestamp
+ );
+ } else {
+ log("Missing uid, localUid, or poll data for submit response.");
+ }
+ break;
+ case PollActionKind.RECEIVE_POLL_ITEM_RESPONSES:
+ log("Handling RECEIVE_POLL_ITEM_RESPONSES");
+ const { id: receivedPollId } = lastAction.payload;
+ const pollCreator = pollsRef.current[receivedPollId]?.createdBy.uid;
+ if (localUid === pollCreator) {
+ log("Received poll response, user is the creator. Syncing...");
+ callDebouncedSyncPoll(
+ pollsRef.current,
+ receivedPollId,
+ PollTaskRequestTypes.SAVE
+ );
+ }
+ break;
+ case PollActionKind.PUBLISH_POLL_ITEM:
+ log("Handling PUBLISH_POLL_ITEM");
+ {
+ const { pollId } = lastAction.payload;
+ syncPollEvt(pollsRef.current, pollId, PollTaskRequestTypes.PUBLISH);
+ }
+ break;
+ case PollActionKind.FINISH_POLL_ITEM:
+ log("Handling FINISH_POLL_ITEM");
+ {
+ const { pollId } = lastAction.payload;
+ syncPollEvt(pollsRef.current, pollId, PollTaskRequestTypes.FINISH);
+ closeCurrentModal();
+ }
+ break;
+ case PollActionKind.DELETE_POLL_ITEM:
+ log("Handling DELETE_POLL_ITEM");
+ {
+ const { pollId } = lastAction.payload;
+ syncPollEvt(pollsRef.current, pollId, PollTaskRequestTypes.DELETE);
+ closeCurrentModal();
+ }
+ break;
+ case PollActionKind.SYNC_COMPLETE:
+ log("Handling SYNC_COMPLETE");
+ const { latestTask, latestPollId } = lastAction.payload;
+ if (
+ latestPollId &&
+ latestTask &&
+ pollsRef.current[latestPollId] &&
+ latestTask === PollTaskRequestTypes.SEND
+ ) {
+ setSidePanel(SidePanelType.None);
+ setModalState({
+ modalType: PollModalType.RESPOND_TO_POLL,
+ id: latestPollId,
+ });
+ }
+ break;
+ default:
+ log(`Unhandled action type: ${lastAction.type}`);
+ break;
+ }
+ } catch (error) {
+ log("Error processing last action:", error);
+ }
+ }, [
+ lastAction,
+ localUid,
+ setSidePanel,
+ syncPollEvt,
+ sendResponseToPollEvt,
+ callDebouncedSyncPoll,
+ closeCurrentModal,
+ ]);
+
+ const startPollForm = () => {
+ log("Opening draft poll modal.");
+ setModalState({
+ modalType: PollModalType.DRAFT_POLL,
+ id: null,
+ });
+ };
+
+ const editPollForm = (pollId: string) => {
+ if (polls[pollId]) {
+ log(`Editing poll form for pollId: ${pollId}`);
+ setModalState({
+ modalType: PollModalType.DRAFT_POLL,
+ id: pollId,
+ });
+ } else {
+ log(`Poll not found for edit: ${pollId}`);
+ }
+ };
+
+ const savePoll = (item: PollItem) => {
+ log("Saving poll item:", item);
+ enhancedDispatch({
+ type: PollActionKind.SAVE_POLL_ITEM,
+ payload: { item: { ...item } },
+ });
+ };
+
+ const addPoll = (item: PollItem) => {
+ log("Adding poll item:", item);
+ enhancedDispatch({
+ type: PollActionKind.ADD_POLL_ITEM,
+ payload: { item: { ...item } },
+ });
+ };
+
+ const sendPoll = (pollId: string) => {
+ if (!pollId || !polls[pollId]) {
+ log("Invalid pollId or poll not found for sending:", pollId);
+ return;
+ }
+ log(`Sending poll with id: ${pollId}`);
+ enhancedDispatch({
+ type: PollActionKind.SEND_POLL_ITEM,
+ payload: { pollId },
+ });
+ };
+
+ const onPollReceived = (
+ newPoll: Poll,
+ pollId: string,
+ task: PollTaskRequestTypes,
+ initialLoad: boolean
+ ) => {
+ log("onPollReceived", newPoll, pollId, task);
+
+ if (!newPoll || !pollId) {
+ log("Invalid newPoll or pollId in onPollReceived:", { newPoll, pollId });
+ return;
+ }
+ const { mergedPolls, deletedPollIds } = mergePolls(newPoll, polls);
+
+ log("Merged polls:", mergedPolls);
+ log("Deleted poll IDs:", deletedPollIds);
+
+ if (Object.keys(mergedPolls).length === 0) {
+ log("No polls left after merge. Resetting state.");
+ enhancedDispatch({ type: PollActionKind.RESET, payload: null });
+ return;
+ }
+
+ if (localUid === newPoll[pollId]?.createdBy.uid) {
+ log("I am the creator, no further action needed.");
+ return;
+ }
+
+ deletedPollIds?.forEach((id: string) => {
+ log(`Deleting poll ID: ${id}`);
+ handlePollTaskRequest(PollTaskRequestTypes.DELETE, id);
+ });
+
+ log("Updating state with merged polls.");
+ Object.values(mergedPolls)
+ .filter((pollItem) => pollItem.status !== PollStatus.LATER)
+ .forEach((pollItem) => {
+ log(`Adding poll ID ${pollItem.id} with status ${pollItem.status}`);
+ addPoll(pollItem);
+ });
+
+ log("Is it an initial load ?:", initialLoad);
+ if (!initialLoad) {
+ enhancedDispatch({
+ type: PollActionKind.SYNC_COMPLETE,
+ payload: {
+ latestTask: task,
+ latestPollId: pollId,
+ },
+ });
+ } else {
+ if (Object.keys(mergedPolls).length > 0) {
+ // Check if there is an active poll
+ log("It is an initial load.");
+ const activePoll = Object.values(mergedPolls).find(
+ (pollItem) => pollItem.status === PollStatus.ACTIVE
+ );
+ if (activePoll) {
+ log("It is an initial load. There is an active poll");
+ setSidePanel(POLL_SIDEBAR_NAME);
+ } else {
+ log("It is an initial load. There are no active poll");
+ }
+ }
+ }
+ };
+
+ const sendResponseToPoll = (item: PollItem, responses: string | string[]) => {
+ log("Sending response to poll:", item, responses);
+ if (
+ (item.type === PollKind.OPEN_ENDED && typeof responses === "string") ||
+ (item.type !== PollKind.OPEN_ENDED && Array.isArray(responses))
+ ) {
+ enhancedDispatch({
+ type: PollActionKind.SUBMIT_POLL_ITEM_RESPONSES,
+ payload: {
+ id: item.id,
+ responses,
+ user: {
+ uid: localUid,
+ name: defaultContent[localUid]?.name || "user",
+ },
+ timestamp: Date.now(),
+ },
+ });
+ } else {
+ throw new Error(
+ "sendResponseToPoll received incorrect type response. Unable to send poll response"
+ );
+ }
+ };
+
+ const onPollResponseReceived = (
+ pollId: string,
+ responses: string | string[],
+ user: {
+ uid: number;
+ name: string;
+ },
+ timestamp: number
+ ) => {
+ log("Received poll response:", { pollId, responses, user, timestamp });
+ enhancedDispatch({
+ type: PollActionKind.RECEIVE_POLL_ITEM_RESPONSES,
+ payload: {
+ id: pollId,
+ responses,
+ user,
+ timestamp,
+ },
+ });
+ };
+
+ const sendPollResults = (pollId: string) => {
+ log(`Sending poll results for pollId: ${pollId}`);
+ syncPollEvt(polls, pollId, PollTaskRequestTypes.SHARE);
+ };
+
+ const handlePollTaskRequest = (
+ task: PollTaskRequestTypes,
+ pollId: string
+ ) => {
+ if (!pollId || !polls[pollId]) {
+ log(
+ "handlePollTaskRequest: Invalid pollId or poll not found for handling",
+ pollId
+ );
+ return;
+ }
+ if (!(task in PollTaskRequestTypes)) {
+ log("handlePollTaskRequest: Invalid valid task", task);
+ return;
+ }
+ log(`Handling poll task request: ${task} for pollId: ${pollId}`);
+ switch (task) {
+ case PollTaskRequestTypes.SEND:
+ if (polls[pollId].status === PollStatus.LATER) {
+ setModalState({
+ modalType: PollModalType.PREVIEW_POLL,
+ id: pollId,
+ });
+ } else {
+ sendPoll(pollId);
+ }
+ break;
+ case PollTaskRequestTypes.SHARE:
+ break;
+ case PollTaskRequestTypes.VIEW_DETAILS:
+ setModalState({
+ modalType: PollModalType.VIEW_POLL_RESULTS,
+ id: pollId,
+ });
+ break;
+ case PollTaskRequestTypes.PUBLISH:
+ enhancedDispatch({
+ type: PollActionKind.PUBLISH_POLL_ITEM,
+ payload: { pollId },
+ });
+ break;
+ case PollTaskRequestTypes.DELETE_CONFIRMATION:
+ setModalState({
+ modalType: PollModalType.DELETE_POLL_CONFIRMATION,
+ id: pollId,
+ });
+ break;
+ case PollTaskRequestTypes.DELETE:
+ enhancedDispatch({
+ type: PollActionKind.DELETE_POLL_ITEM,
+ payload: { pollId },
+ });
+ break;
+ case PollTaskRequestTypes.FINISH_CONFIRMATION:
+ setModalState({
+ modalType: PollModalType.END_POLL_CONFIRMATION,
+ id: pollId,
+ });
+ break;
+ case PollTaskRequestTypes.FINISH:
+ enhancedDispatch({
+ type: PollActionKind.FINISH_POLL_ITEM,
+ payload: { pollId },
+ });
+ break;
+ case PollTaskRequestTypes.EXPORT:
+ enhancedDispatch({
+ type: PollActionKind.EXPORT_POLL_ITEM,
+ payload: { pollId },
+ });
+ break;
+ default:
+ log(`Unhandled task type: ${task}`);
+ break;
+ }
+ };
+
+ const value = {
+ polls,
+ startPollForm,
+ editPollForm,
+ sendPoll,
+ savePoll,
+ onPollReceived,
+ onPollResponseReceived,
+ sendResponseToPoll,
+ sendPollResults,
+ handlePollTaskRequest,
+ modalState,
+ closeCurrentModal,
+ isHost,
+ };
+
+ return {children};
+}
+
+function usePoll() {
+ const context = React.useContext(PollContext);
+ if (!context) {
+ throw new Error("usePoll must be used within a PollProvider");
+ }
+ return context;
+}
+
+export {
+ PollProvider,
+ usePoll,
+ PollActionKind,
+ PollKind,
+ PollStatus,
+ PollModalType,
+ PollTaskRequestTypes,
+};
+
+export type { Poll, PollItem, PollFormErrors, PollItemOptionItem };
diff --git a/polling/context/poll-events.tsx b/polling/context/poll-events.tsx
new file mode 100644
index 0000000..0895927
--- /dev/null
+++ b/polling/context/poll-events.tsx
@@ -0,0 +1,224 @@
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useState,
+ useCallback,
+ useMemo,
+ useRef,
+} from 'react';
+import {Poll, PollItem, PollTaskRequestTypes, usePoll} from './poll-context';
+import {customEvents as events, PersistanceLevel} from 'customization-api';
+import {log} from '../helpers';
+
+enum PollEventNames {
+ polls = 'POLLS',
+ pollResponse = 'POLL_RESPONSE',
+}
+
+type sendResponseToPollEvtFunction = (
+ item: PollItem,
+ responses: string | string[],
+ user: {name: string; uid: number},
+ timestamp: number,
+) => void;
+
+interface PollEventsContextValue {
+ syncPollEvt: (
+ polls: Poll,
+ pollId: string,
+ task: PollTaskRequestTypes,
+ ) => void;
+ sendResponseToPollEvt: sendResponseToPollEvtFunction;
+}
+
+const PollEventsContext = createContext(null);
+PollEventsContext.displayName = 'PollEventsContext';
+
+// Event Dispatcher
+function PollEventsProvider({children}: {children?: React.ReactNode}) {
+ // Sync poll event handler
+ const syncPollEvt = useCallback(
+ (polls: Poll, pollId: string, task: PollTaskRequestTypes) => {
+ log('syncPollEvt called', {polls, pollId, task});
+ try {
+ if (!polls || !pollId || !task) {
+ throw new Error('Invalid arguments provided to syncPollEvt.');
+ }
+ events.send(
+ PollEventNames.polls,
+ JSON.stringify({
+ state: {...polls},
+ pollId: pollId,
+ task,
+ }),
+ PersistanceLevel.Channel,
+ );
+ log('Poll sync successful', {pollId, task});
+ } catch (error) {
+ console.error('Error while syncing poll: ', error);
+ }
+ },
+ [],
+ );
+
+ // Send response to poll handler
+ const sendResponseToPollEvt: sendResponseToPollEvtFunction = useCallback(
+ (item, responses, user, timestamp) => {
+ log('sendResponseToPollEvt called', {item, responses, user, timestamp});
+ try {
+ if (!item || !item.id || !responses || !user.uid) {
+ throw new Error(
+ 'Invalid arguments provided to sendResponseToPollEvt.',
+ );
+ }
+ if (!item?.createdBy?.uid) {
+ throw new Error(
+ 'Poll createdBy is null, cannot send response to creator',
+ );
+ }
+ events.send(
+ PollEventNames.pollResponse,
+ JSON.stringify({
+ id: item.id,
+ responses,
+ user,
+ timestamp,
+ }),
+ PersistanceLevel.None,
+ item.createdBy.uid,
+ );
+ log('Poll response sent successfully', {pollId: item.id});
+ } catch (error) {
+ console.error('Error while sending a poll response: ', error);
+ }
+ },
+ [],
+ );
+
+ const value = useMemo(
+ () => ({
+ syncPollEvt,
+ sendResponseToPollEvt,
+ }),
+ [syncPollEvt, sendResponseToPollEvt],
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+function usePollEvents() {
+ const context = useContext(PollEventsContext);
+ if (!context) {
+ throw new Error('usePollEvents must be used within PollEventsProvider.');
+ }
+ return context;
+}
+
+// Event Subscriber
+const PollEventsSubscriberContext = createContext(null);
+PollEventsSubscriberContext.displayName = 'PollEventsContext';
+
+function PollEventsSubscriber({children}: {children?: React.ReactNode}) {
+ const {onPollReceived, onPollResponseReceived} = usePoll();
+ // State variable to track whether the initial load has occurred
+ // State variable to track whether the initial load has occurred
+ const [initialized, setInitialized] = useState(false);
+ const [initialLoadComplete, setInitialLoadComplete] = useState(false);
+
+ // Use refs to hold the stable references of callbacks
+ const onPollReceivedRef = useRef(onPollReceived);
+ const onPollResponseReceivedRef = useRef(onPollResponseReceived);
+
+ // Keep refs updated with latest callbacks
+ useEffect(() => {
+ onPollReceivedRef.current = onPollReceived;
+ onPollResponseReceivedRef.current = onPollResponseReceived;
+ }, [onPollReceived, onPollResponseReceived]);
+
+ useEffect(() => {
+ if (!onPollReceivedRef.current || !onPollResponseReceivedRef.current) {
+ log('PollEventsSubscriber ref not intialized.');
+ return;
+ }
+ log('PollEventsSubscriber useEffect triggered.');
+ log('PollEventsSubscriber is app initialized ?', initialized);
+ let initialLoadTimeout: ReturnType;
+ // Set initialLoadTimeout only if initialLoadComplete is false
+ if (!initialLoadComplete) {
+ log('Setting initial load timeout.');
+ initialLoadTimeout = setTimeout(() => {
+ log('Initial load timeout reached. Marking initial load as complete.');
+ setInitialLoadComplete(true);
+ }, 3000); // Adjust the timeout duration as necessary
+ }
+
+ events.on(PollEventNames.polls, args => {
+ try {
+ log('PollEventNames.polls event received', args);
+ const {payload} = args;
+ const data = JSON.parse(payload);
+ const {state, pollId, task} = data;
+ log('Poll data received and parsed successfully:', data);
+ // Determine if it's the initial load or a runtime update
+ if (!initialized && !initialLoadComplete) {
+ log('Initial load detected.');
+ // Call onPollReceived with an additional parameter or flag for initial load
+ onPollReceivedRef.current(state, pollId, task, true); // true indicates it's an initial load
+ setInitialized(true);
+ // Clear the initial load timeout since we have received the initial state
+ clearTimeout(initialLoadTimeout);
+ } else {
+ log('Runtime update detected');
+ onPollReceivedRef.current(state, pollId, task, false); // false indicates it's a runtime update
+ }
+ // switch (action) {
+ // case PollEventActions.savePoll:
+ // log('on poll saved');
+ // onPollReceived(state, pollId, task);
+ // break;
+ // case PollEventActions.sendPoll:
+ // log('on poll received');
+ // onPollReceived(state, pollId, task);
+ // break;
+ // default:
+ // break;
+ // }
+ } catch (error) {
+ log('Error handling poll event:', error);
+ }
+ });
+ events.on(PollEventNames.pollResponse, args => {
+ try {
+ log('PollEventNames.pollResponse event received', args);
+ const {payload} = args;
+ const data = JSON.parse(payload);
+ log('poll response received', data);
+ const {id, responses, user, timestamp} = data;
+ log('Poll response data parsed successfully:', data);
+ onPollResponseReceivedRef.current(id, responses, user, timestamp);
+ } catch (error) {
+ log('Error handling poll response event:', error);
+ }
+ });
+
+ return () => {
+ log('Cleaning up PollEventsSubscriber event listeners.');
+ events.off(PollEventNames.polls);
+ events.off(PollEventNames.pollResponse);
+ clearTimeout(initialLoadTimeout);
+ };
+ }, [initialized, initialLoadComplete]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export {usePollEvents, PollEventsProvider, PollEventsSubscriber};
diff --git a/polling/helpers.ts b/polling/helpers.ts
new file mode 100644
index 0000000..2bab750
--- /dev/null
+++ b/polling/helpers.ts
@@ -0,0 +1,219 @@
+import { isMobileUA, isWeb } from "customization-api";
+import { Poll, PollItemOptionItem, PollKind } from "./context/poll-context";
+import pollIcons from "./poll-icons";
+
+function log(...args: any[]) {
+ console.log("[Custom-Polling::]", ...args);
+}
+
+function addVote(
+ responses: string[],
+ options: PollItemOptionItem[],
+ user: { name: string; uid: number },
+ timestamp: number
+): PollItemOptionItem[] {
+ return options.map((option: PollItemOptionItem) => {
+ // Count how many times the value appears in the strings array
+ const exists = responses.includes(option.value);
+ const isVoted = option.votes.find((item) => item.uid === user.uid);
+ if (exists && !isVoted) {
+ // Creating a new object explicitly
+ const newOption: PollItemOptionItem = {
+ ...option,
+ ...option,
+ votes: [
+ ...option.votes,
+ {
+ ...user,
+ timestamp,
+ },
+ ],
+ };
+ return newOption;
+ }
+ // If no matches, return the option as is
+ return option;
+ });
+}
+
+function calculatePercentage(
+ options: PollItemOptionItem[]
+): PollItemOptionItem[] {
+ const totalVotes = options.reduce(
+ (total, item) => total + item.votes.length,
+ 0
+ );
+ if (totalVotes === 0) {
+ // As none of the users have voted, there is no need to calulate the percentage,
+ // we can return the options as it is
+ return options;
+ }
+ return options.map((option: PollItemOptionItem) => {
+ let percentage = 0;
+ if (option.votes.length > 0) {
+ percentage = (option.votes.length / totalVotes) * 100;
+ }
+ // Creating a new object explicitly
+ const newOption: PollItemOptionItem = {
+ ...option,
+ percent: percentage.toFixed(0),
+ };
+ return newOption;
+ }) as PollItemOptionItem[];
+}
+
+function arrayToCsv(question: string, data: PollItemOptionItem[]): string {
+ const headers = ["Option", "Votes", "Percent"]; // Define the headers
+ const rows = data.map((item) => {
+ const count = item.votes.length;
+ // Handle missing or undefined value
+ const voteText = item.text ? `"${item.text}"` : '""';
+ const votesCount = count !== undefined ? count : "0";
+ const votePercent = item.percent !== undefined ? `${item.percent}%` : "0%";
+
+ return `${voteText},${votesCount},${votePercent}`;
+ });
+ // Include poll question at the top
+ const pollQuestion = `Poll Question: "${question}"`;
+ return [pollQuestion, "", headers.join(","), ...rows].join("\n");
+}
+
+function downloadCsv(data: string, filename: string = "data.csv"): void {
+ const blob = new Blob([data], { type: "text/csv;charset=utf-8;" });
+ const link = document.createElement("a");
+ const url = URL.createObjectURL(blob);
+
+ link.setAttribute("href", url);
+ link.setAttribute("download", filename);
+ link.setAttribute("target", "_blank");
+ link.style.visibility = "hidden";
+
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+}
+
+function capitalizeFirstLetter(sentence: string): string {
+ return sentence.charAt(0).toUpperCase() + sentence.slice(1).toLowerCase();
+}
+
+function hasUserVoted(options: PollItemOptionItem[], uid: number): boolean {
+ // Loop through each option and check the votes array
+ return options.some((option) =>
+ option.votes.some((vote) => vote.uid === uid)
+ );
+}
+
+type MergePollsResult = {
+ mergedPolls: Poll;
+ deletedPollIds: string[];
+};
+
+function mergePolls(newPoll: Poll, oldPoll: Poll): MergePollsResult {
+ // Merge and discard absent properties
+
+ // 1. Start with a copy of the current polls state
+ const mergedPolls: Poll = { ...oldPoll };
+
+ // 2. Array to track deleted poll IDs
+ const deletedPollIds: string[] = [];
+
+ // 3. Add or update polls from newPolls
+ Object.keys(newPoll).forEach((pollId) => {
+ mergedPolls[pollId] = newPoll[pollId]; // Add or update each poll from newPolls
+ });
+
+ // 4. Remove polls that are not in newPolls and track deleted poll IDs
+ Object.keys(oldPoll).forEach((pollId) => {
+ if (!(pollId in newPoll)) {
+ delete mergedPolls[pollId]; // Delete polls that are no longer present in newPolls
+ deletedPollIds.push(pollId); // Track deleted poll ID
+ }
+ });
+
+ // 5. Return the merged polls and deleted poll IDs
+ return { mergedPolls, deletedPollIds };
+}
+
+function getPollTypeIcon(type: PollKind): string {
+ if (type === PollKind.OPEN_ENDED) {
+ return pollIcons.question;
+ }
+ if (type === PollKind.YES_NO) {
+ return pollIcons["like-dislike"];
+ }
+ if (type === PollKind.MCQ) {
+ return pollIcons.mcq;
+ }
+ return pollIcons.question;
+}
+
+function getPollTypeDesc(type: PollKind, multiple_response?: boolean): string {
+ if (type === PollKind.OPEN_ENDED) {
+ return "Open Ended";
+ }
+ if (type === PollKind.YES_NO) {
+ return "Select Any One";
+ }
+ if (type === PollKind.MCQ) {
+ if (multiple_response) {
+ return "MCQ - Select One or More";
+ }
+ return "MCQ - Select Any One";
+ }
+ return "None";
+}
+
+function formatTimestampToTime(timestamp: number): string {
+ // Create a new Date object using the timestamp
+ const date = new Date(timestamp);
+ // Get hours and minutes from the Date object
+ let hours = date.getHours();
+ const minutes = date.getMinutes();
+ // Determine if it's AM or PM
+ const ampm = hours >= 12 ? "PM" : "AM";
+ // Convert hours to 12-hour format
+ hours = hours % 12;
+ hours = hours ? hours : 12; // The hour '0' should be '12'
+ // Format minutes to always have two digits
+ const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes;
+ // Construct the formatted time string
+ return `${hours}:${formattedMinutes} ${ampm}`;
+}
+
+function calculateTotalVotes(options: Array): number {
+ // Use reduce to sum up the length of the votes array for each option
+ return options.reduce((total, option) => total + option.votes.length, 0);
+}
+
+const debounce = void>(
+ func: T,
+ delay: number = 300
+) => {
+ let debounceTimer: ReturnType;
+ return function (this: ThisParameterType, ...args: Parameters) {
+ clearTimeout(debounceTimer);
+ debounceTimer = setTimeout(() => {
+ func.apply(this, args);
+ }, delay);
+ };
+};
+
+const isWebOnly = () => isWeb() && !isMobileUA();
+
+export {
+ log,
+ mergePolls,
+ hasUserVoted,
+ downloadCsv,
+ arrayToCsv,
+ addVote,
+ calculatePercentage,
+ capitalizeFirstLetter,
+ getPollTypeDesc,
+ formatTimestampToTime,
+ calculateTotalVotes,
+ debounce,
+ getPollTypeIcon,
+ isWebOnly,
+};
diff --git a/polling/hook/useButtonState.tsx b/polling/hook/useButtonState.tsx
new file mode 100644
index 0000000..8ca5914
--- /dev/null
+++ b/polling/hook/useButtonState.tsx
@@ -0,0 +1,68 @@
+// useButtonState.ts
+import {useState, useCallback, useRef, useEffect} from 'react';
+
+interface useButtonStateReturn {
+ buttonText: string;
+ isSubmitting: boolean;
+ submitted: boolean;
+ handleSubmit: (submitFunction?: () => Promise | void) => void;
+ resetState: () => void;
+}
+
+export function useButtonState(
+ initialText: string = 'Submit',
+ submittingText: string = 'Submitting...',
+ submittedText: string = 'Submitted',
+): useButtonStateReturn {
+ const [buttonText, setButtonText] = useState(initialText);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [submitted, setIsSubmitted] = useState(false);
+ const timeoutRef = useRef(null); // Reference to store timeout ID
+
+ // Handles the submission process
+ const handleSubmit = useCallback(
+ async (submitFunction?: () => Promise | void) => {
+ setIsSubmitting(true);
+ setButtonText(submittingText);
+
+ try {
+ // Execute the submit function if provided
+ if (submitFunction) {
+ await submitFunction();
+ }
+
+ // After submission, update the text to "Submitted" with delay
+ timeoutRef.current = setTimeout(() => {
+ setIsSubmitted(true);
+ setButtonText(submittedText);
+ }, 1000);
+ } catch (error) {
+ // Handle error (e.g., reset button text or show error)
+ setButtonText(initialText);
+ } finally {
+ // Restore the submit state after completion
+ timeoutRef.current = setTimeout(() => {
+ setIsSubmitting(false);
+ }, 1000);
+ }
+ },
+ [initialText, submittingText, submittedText],
+ );
+
+ // Cleanup function to clear timeouts if the component unmounts
+ useEffect(() => {
+ return () => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+ };
+ }, []);
+
+ // Reset the state to the initial values
+ const resetState = () => {
+ setButtonText(initialText);
+ setIsSubmitting(false);
+ };
+
+ return {buttonText, isSubmitting, submitted, handleSubmit, resetState};
+}
diff --git a/polling/hook/useCountdownTimer.tsx b/polling/hook/useCountdownTimer.tsx
new file mode 100644
index 0000000..238f3ad
--- /dev/null
+++ b/polling/hook/useCountdownTimer.tsx
@@ -0,0 +1,45 @@
+import {useEffect, useState, useRef} from 'react';
+
+const useCountdown = (targetDate: number) => {
+ const countDownDate = new Date(targetDate).getTime();
+ const intervalRef = useRef(null); // Add a ref to store the interval id
+
+ const [countDown, setCountDown] = useState(
+ countDownDate - new Date().getTime(),
+ );
+
+ useEffect(() => {
+ intervalRef.current = setInterval(() => {
+ setCountDown(_ => {
+ const newCountDown = countDownDate - new Date().getTime();
+ if (newCountDown <= 0) {
+ clearInterval(intervalRef.current!);
+ return 0;
+ }
+ return newCountDown;
+ });
+ }, 1000);
+
+ return () => {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ }
+ };
+ }, [countDownDate]);
+
+ return getReturnValues(countDown);
+};
+
+const getReturnValues = countDown => {
+ // calculate time left
+ const days = Math.floor(countDown / (1000 * 60 * 60 * 24));
+ const hours = Math.floor(
+ (countDown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60),
+ );
+ const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60));
+ const seconds = Math.floor((countDown % (1000 * 60)) / 1000);
+
+ return [days, hours, minutes, seconds];
+};
+
+export {useCountdown};
diff --git a/polling/hook/usePollForm.tsx b/polling/hook/usePollForm.tsx
new file mode 100644
index 0000000..b794fa3
--- /dev/null
+++ b/polling/hook/usePollForm.tsx
@@ -0,0 +1,162 @@
+import {useState, useEffect, useRef, useCallback, SetStateAction} from 'react';
+import {PollItem, PollKind} from '../context/poll-context';
+// import {useLocalUid} from 'customization-api';
+
+interface UsePollFormProps {
+ pollItem: PollItem;
+ initialSubmitted?: boolean;
+ onFormSubmit: (responses: string | string[]) => void;
+ onFormSubmitComplete?: () => void;
+}
+
+interface PollFormInput {
+ pollItem: PollItem;
+ selectedOption: string | null;
+ selectedOptions: string[];
+ handleRadioSelect: (option: string) => void;
+ handleCheckboxToggle: (option: string) => void;
+ answer: string;
+ setAnswer: React.Dispatch>;
+}
+interface PollFormButton {
+ onSubmit: () => void;
+ buttonVisible: boolean;
+ buttonStatus: ButtonStatus;
+ buttonText: string;
+ submitDisabled: boolean;
+}
+interface UsePollFormReturn
+ extends Omit,
+ PollFormButton {}
+
+type ButtonStatus = 'initial' | 'selected' | 'submitting' | 'submitted';
+
+export function usePollForm({
+ pollItem,
+ initialSubmitted = false,
+ onFormSubmit,
+ onFormSubmitComplete,
+}: UsePollFormProps): UsePollFormReturn {
+ const [selectedOption, setSelectedOption] = useState(null);
+ const [selectedOptions, setSelectedOptions] = useState([]);
+ const [buttonVisible, setButtonVisible] = useState(!initialSubmitted);
+
+ const [answer, setAnswer] = useState('');
+ const [buttonStatus, setButtonStatus] = useState(
+ initialSubmitted ? 'submitted' : 'initial',
+ );
+
+ const timeoutRef = useRef(null);
+ // const localUid = useLocalUid();
+
+ // Set state for radio button selection
+ const handleRadioSelect = useCallback((option: string) => {
+ setSelectedOption(option);
+ setButtonStatus('selected'); // Mark the button state as selected
+ }, []);
+
+ // Set state for checkbox toggle
+ const handleCheckboxToggle = useCallback((value: string) => {
+ setSelectedOptions(prevSelectedOptions => {
+ const newSelectedOptions = prevSelectedOptions.includes(value)
+ ? prevSelectedOptions.filter(option => option !== value)
+ : [...prevSelectedOptions, value];
+ setButtonStatus(newSelectedOptions.length > 0 ? 'selected' : 'initial');
+ return newSelectedOptions;
+ });
+ }, []);
+
+ // Handle form submission
+ const onSubmit = useCallback(() => {
+ setButtonStatus('submitting');
+
+ // Logic to handle form submission
+ if (pollItem.multiple_response) {
+ if (selectedOptions.length === 0) {
+ return;
+ }
+ onFormSubmit(selectedOptions);
+ } else {
+ if (!selectedOption) {
+ return;
+ }
+ onFormSubmit([selectedOption]);
+ }
+
+ // Simulate submission delay and complete the process
+ timeoutRef.current = setTimeout(() => {
+ setButtonStatus('submitted');
+
+ // Trigger the form submit complete callback, if provided
+ if (onFormSubmitComplete) {
+ timeoutRef.current = setTimeout(() => {
+ // Call the onFormSubmitComplete callback
+ onFormSubmitComplete();
+ // Hide the button after submission
+ setButtonVisible(false);
+ }, 2000);
+ } else {
+ // If no callback is provided, immediately hide the button without waiting
+ setButtonVisible(false);
+ // Time for displaying "Submitted" before calling onFormSubmitComplete
+ }
+ }, 1000);
+ }, [
+ selectedOption,
+ selectedOptions,
+ pollItem,
+ onFormSubmit,
+ onFormSubmitComplete,
+ ]);
+
+ useEffect(() => {
+ return () => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+ };
+ }, []);
+
+ // Derive button text from button status
+ const buttonText = (() => {
+ switch (buttonStatus) {
+ case 'initial':
+ return 'Submit';
+ case 'selected':
+ return 'Submit';
+ case 'submitting':
+ return 'Submitting...';
+ case 'submitted':
+ return 'Submitted';
+ }
+ })();
+
+ // Define when the submit button should be disabled
+ const submitDisabled =
+ buttonStatus === 'submitting' ||
+ buttonStatus === 'submitted' ||
+ (pollItem.type === PollKind.OPEN_ENDED && answer?.trim() === '') ||
+ (pollItem.type === PollKind.YES_NO && !selectedOption) ||
+ (pollItem.type === PollKind.MCQ &&
+ !pollItem.multiple_response &&
+ !selectedOption) ||
+ (pollItem.type === PollKind.MCQ &&
+ pollItem.multiple_response &&
+ selectedOptions.length === 0);
+
+ return {
+ selectedOption,
+ selectedOptions,
+ handleRadioSelect,
+ handleCheckboxToggle,
+ onSubmit,
+ buttonVisible,
+ buttonStatus,
+ buttonText,
+ answer,
+ setAnswer,
+ submitDisabled,
+ };
+}
+
+export type {PollFormInput, PollFormButton};
diff --git a/polling/hook/usePollPermissions.tsx b/polling/hook/usePollPermissions.tsx
new file mode 100644
index 0000000..fbca9e2
--- /dev/null
+++ b/polling/hook/usePollPermissions.tsx
@@ -0,0 +1,73 @@
+import {useMemo} from 'react';
+import {useLocalUid, useRoomInfo} from 'customization-api';
+import {PollItem, PollStatus} from '../context/poll-context';
+import {isWebOnly} from '../helpers';
+
+interface PollPermissions {
+ canCreate: boolean;
+ canEdit: boolean;
+ canEnd: boolean;
+ canViewWhoVoted: boolean;
+ canViewVotesPercent: boolean;
+ canViewPollDetails: boolean;
+}
+
+interface UsePollPermissionsProps {
+ pollItem?: PollItem; // The current poll object
+}
+
+export const usePollPermissions = ({
+ pollItem,
+}: UsePollPermissionsProps): PollPermissions => {
+ const localUid = useLocalUid();
+ const {
+ data: {isHost},
+ } = useRoomInfo();
+ // Calculate permissions using useMemo to optimize performance
+ const permissions = useMemo(() => {
+ // Check if the current user is the creator of the poll
+ const isPollCreator = pollItem?.createdBy.uid === localUid || false;
+ // Determine if the user is both a host and the creator of the poll
+ const isPollHost = isHost && isPollCreator;
+ // Determine if the user is a host but not the creator of the poll (co-host)
+ // const isPollCoHost = isHost && !isPollCreator;
+ // Determine if the user is an attendee (not a host and not the creator)
+ const isPollAttendee = !isHost && !isPollCreator;
+
+ // Determine if the user can create the poll (only the host can create)
+ const canCreate = isHost && isWebOnly();
+ // Determine if the user can edit the poll (only the poll host can edit)
+ const canEdit = isPollHost && isWebOnly();
+ // Determine if the user can end the poll (only the poll host can end an active poll)
+ const canEnd = isPollHost && pollItem?.status === PollStatus.ACTIVE;
+
+ // Determine if the user can view the percentage of votes
+ // - Hosts can always view the percentage of votes
+ // - Co-hosts and attendees can view it if share_host or share_attendee is true respectively
+ const canViewVotesPercent = true;
+ // isPollHost ||
+ // (isPollCoHost && pollItem.share_host) ||
+ // (isPollAttendee && pollItem.share_attendee);
+
+ // Determine if the user can view poll details (all hosts can view details, attendees cannot)
+ const canViewPollDetails = true;
+ // isPollHost || isPollCoHost;
+
+ // Determine if the user can view who voted
+ // - If `pollItem.anonymous` is true, no one can view who voted
+ // - If `pollItem.anonymous` is false, only hosts and co-hosts can view who voted, attendees cannot
+ const canViewWhoVoted = !isPollAttendee;
+ // canViewPollDetails && !pollItem?.anonymous;
+
+ return {
+ canCreate,
+ canEdit,
+ canEnd,
+ canViewVotesPercent,
+ canViewWhoVoted,
+ canViewPollDetails,
+ };
+ }, [localUid, pollItem, isHost]);
+
+ return permissions;
+};
diff --git a/polling/poll-icons.ts b/polling/poll-icons.ts
new file mode 100644
index 0000000..ba5422a
--- /dev/null
+++ b/polling/poll-icons.ts
@@ -0,0 +1,31 @@
+interface PollIconsInterface {
+ mcq: string;
+ 'like-dislike': string;
+ question: string;
+ 'bar-chart': string;
+ anonymous: string;
+ 'stop-watch': string;
+ group: string;
+ 'co-host': string;
+}
+
+const pollIcons: PollIconsInterface = {
+ mcq: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgaWQ9InBvbGwtaWNvbnMiPgo8bWFzayBpZD0ibWFzazBfOTYzN182NjY0OCIgc3R5bGU9Im1hc2stdHlwZTphbHBoYSIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeD0iMCIgeT0iMCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij4KPHJlY3QgaWQ9IkJvdW5kaW5nIGJveCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiBmaWxsPSIjRkY0MTREIi8+CjwvbWFzaz4KPGcgbWFzaz0idXJsKCNtYXNrMF85NjM3XzY2NjQ4KSI+CjxwYXRoIGlkPSJjaGVja2xpc3QiIGQ9Ik01LjUyNDggMTYuMTczOUw5LjA3NDggMTIuNjIzOUM5LjI3NDggMTIuNDIzOSA5LjUwODE0IDEyLjMyODEgOS43NzQ4IDEyLjMzNjRDMTAuMDQxNSAxMi4zNDQ4IDEwLjI3NDggMTIuNDQ4OSAxMC40NzQ4IDEyLjY0ODlDMTAuNjU4MSAxMi44NDg5IDEwLjc0OTggMTMuMDgyMyAxMC43NDk4IDEzLjM0ODlDMTAuNzQ5OCAxMy42MTU2IDEwLjY1ODEgMTMuODQ4OSAxMC40NzQ4IDE0LjA0ODlMNi4yNDk4IDE4LjI5ODlDNi4wNDk4IDE4LjQ5ODkgNS44MTY0NyAxOC41OTg5IDUuNTQ5OCAxOC41OTg5QzUuMjgzMTQgMTguNTk4OSA1LjA0OTggMTguNDk4OSA0Ljg0OTggMTguMjk4OUwyLjY5OTggMTYuMTQ4OUMyLjUxNjQ3IDE1Ljk2NTYgMi40MjQ4IDE1LjczMjMgMi40MjQ4IDE1LjQ0ODlDMi40MjQ4IDE1LjE2NTYgMi41MTY0NyAxNC45MzIzIDIuNjk5OCAxNC43NDg5QzIuODgzMTQgMTQuNTY1NiAzLjExNjQ3IDE0LjQ3MzkgMy4zOTk4IDE0LjQ3MzlDMy42ODMxNCAxNC40NzM5IDMuOTE2NDcgMTQuNTY1NiA0LjA5OTggMTQuNzQ4OUw1LjUyNDggMTYuMTczOVpNNS41MjQ4IDguMTczOTRMOS4wNzQ4IDQuNjIzOTRDOS4yNzQ4IDQuNDIzOTQgOS41MDgxNCA0LjMyODEgOS43NzQ4IDQuMzM2NDRDMTAuMDQxNSA0LjM0NDc3IDEwLjI3NDggNC40NDg5NCAxMC40NzQ4IDQuNjQ4OTRDMTAuNjU4MSA0Ljg0ODk0IDEwLjc0OTggNS4wODIyNyAxMC43NDk4IDUuMzQ4OTRDMTAuNzQ5OCA1LjYxNTYgMTAuNjU4MSA1Ljg0ODk0IDEwLjQ3NDggNi4wNDg5NEw2LjI0OTggMTAuMjk4OUM2LjA0OTggMTAuNDk4OSA1LjgxNjQ3IDEwLjU5ODkgNS41NDk4IDEwLjU5ODlDNS4yODMxNCAxMC41OTg5IDUuMDQ5OCAxMC40OTg5IDQuODQ5OCAxMC4yOTg5TDIuNjk5OCA4LjE0ODk0QzIuNTE2NDcgNy45NjU2IDIuNDI0OCA3LjczMjI3IDIuNDI0OCA3LjQ0ODk0QzIuNDI0OCA3LjE2NTYgMi41MTY0NyA2LjkzMjI3IDIuNjk5OCA2Ljc0ODk0QzIuODgzMTQgNi41NjU2IDMuMTE2NDcgNi40NzM5NCAzLjM5OTggNi40NzM5NEMzLjY4MzE0IDYuNDczOTQgMy45MTY0NyA2LjU2NTYgNC4wOTk4IDYuNzQ4OTRMNS41MjQ4IDguMTczOTRaTTEzLjk5OTggMTYuOTk4OUMxMy43MTY1IDE2Ljk5ODkgMTMuNDc5IDE2LjkwMzEgMTMuMjg3MyAxNi43MTE0QzEzLjA5NTYgMTYuNTE5OCAxMi45OTk4IDE2LjI4MjMgMTIuOTk5OCAxNS45OTg5QzEyLjk5OTggMTUuNzE1NiAxMy4wOTU2IDE1LjQ3ODEgMTMuMjg3MyAxNS4yODY0QzEzLjQ3OSAxNS4wOTQ4IDEzLjcxNjUgMTQuOTk4OSAxMy45OTk4IDE0Ljk5ODlIMjAuOTk5OEMyMS4yODMxIDE0Ljk5ODkgMjEuNTIwNiAxNS4wOTQ4IDIxLjcxMjMgMTUuMjg2NEMyMS45MDQgMTUuNDc4MSAyMS45OTk4IDE1LjcxNTYgMjEuOTk5OCAxNS45OTg5QzIxLjk5OTggMTYuMjgyMyAyMS45MDQgMTYuNTE5OCAyMS43MTIzIDE2LjcxMTRDMjEuNTIwNiAxNi45MDMxIDIxLjI4MzEgMTYuOTk4OSAyMC45OTk4IDE2Ljk5ODlIMTMuOTk5OFpNMTMuOTk5OCA4Ljk5ODk0QzEzLjcxNjUgOC45OTg5NCAxMy40NzkgOC45MDMxIDEzLjI4NzMgOC43MTE0NEMxMy4wOTU2IDguNTE5NzcgMTIuOTk5OCA4LjI4MjI3IDEyLjk5OTggNy45OTg5NEMxMi45OTk4IDcuNzE1NiAxMy4wOTU2IDcuNDc4MSAxMy4yODczIDcuMjg2NDRDMTMuNDc5IDcuMDk0NzcgMTMuNzE2NSA2Ljk5ODk0IDEzLjk5OTggNi45OTg5NEgyMC45OTk4QzIxLjI4MzEgNi45OTg5NCAyMS41MjA2IDcuMDk0NzcgMjEuNzEyMyA3LjI4NjQ0QzIxLjkwNCA3LjQ3ODEgMjEuOTk5OCA3LjcxNTYgMjEuOTk5OCA3Ljk5ODk0QzIxLjk5OTggOC4yODIyNyAyMS45MDQgOC41MTk3NyAyMS43MTIzIDguNzExNDRDMjEuNTIwNiA4LjkwMzEgMjEuMjgzMSA4Ljk5ODk0IDIwLjk5OTggOC45OTg5NEgxMy45OTk4WiIgZmlsbD0iI0JEQ0ZEQiIvPgo8L2c+CjwvZz4KPC9zdmc+Cg==',
+ 'like-dislike':
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgaWQ9InBvbGwtaWNvbnMiPgo8bWFzayBpZD0ibWFzazBfOTYzN181Nzk5NSIgc3R5bGU9Im1hc2stdHlwZTphbHBoYSIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeD0iMCIgeT0iMCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij4KPHJlY3QgaWQ9IkJvdW5kaW5nIGJveCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiBmaWxsPSIjRkY0MTREIi8+CjwvbWFzaz4KPGcgbWFzaz0idXJsKCNtYXNrMF85NjM3XzU3OTk1KSI+CjxwYXRoIGlkPSJ0aHVtYnNfdXBfZG93biIgZD0iTTAuNTg3NSAxMy40MTQxQzAuOTc5MTY3IDEzLjgwNTcgMS40NSAxNC4wMDE2IDIgMTQuMDAxNkg4LjI1QzguNTUgMTQuMDAxNiA4LjgyOTE3IDEzLjkyMjQgOS4wODc1IDEzLjc2NDFDOS4zNDU4MyAxMy42MDU3IDkuNTMzMzMgMTMuMzg0OSA5LjY1IDEzLjEwMTZMMTEuOSA3LjgwMTU2QzExLjkzMzMgNy43MTgyMyAxMS45NTgzIDcuNjMwNzMgMTEuOTc1IDcuNTM5MDZDMTEuOTkxNyA3LjQ0NzQgMTIgNy4zNTE1NiAxMiA3LjI1MTU2VjYuMDAxNTZDMTIgNS43MTgyMyAxMS45MDQyIDUuNDgwNzMgMTEuNzEyNSA1LjI4OTA2QzExLjUyMDggNS4wOTc0IDExLjI4MzMgNS4wMDE1NiAxMSA1LjAwMTU2SDUuOEw3LjEyNSAyLjEyNjU2QzcuMjA4MzMgMS42MDk5IDcuMDkxNjcgMS4xODQ5IDYuNzc1IDAuODUxNTYyQzYuNDU4MzMgMC41MTgyMjkgNi4wODMzMyAwLjM1MTU2MiA1LjY1IDAuMzUxNTYyQzUuNDY2NjcgMC4zNTE1NjIgNS4yODMzMyAwLjM4OTA2MiA1LjEgMC40NjQwNjJDNC45MTY2NyAwLjUzOTA2MiA0Ljc1IDAuNjUxNTYzIDQuNiAwLjgwMTU2M0wwLjQ1IDQuOTUxNTZDMC4zMTY2NjcgNS4wODQ5IDAuMjA4MzMzIDUuMjQzMjMgMC4xMjUgNS40MjY1NkMwLjA0MTY2NjcgNS42MDk5IDAgNS44MDE1NiAwIDYuMDAxNTZWMTIuMDAxNkMwIDEyLjU1MTYgMC4xOTU4MzMgMTMuMDIyNCAwLjU4NzUgMTMuNDE0MVoiIGZpbGw9IiNCRENGREIiLz4KPHBhdGggaWQ9InRodW1ic191cF9kb3duXzIiIGQ9Ik0xMyAxOUMxMi43MTY3IDE5IDEyLjQ3OTIgMTguOTA0MiAxMi4yODc1IDE4LjcxMjVDMTIuMDk1OCAxOC41MjA4IDEyIDE4LjI4MzMgMTIgMThWMTYuNzVDMTIgMTYuNjUgMTIuMDA4MyAxNi41NTQyIDEyLjAyNSAxNi40NjI1QzEyLjA0MTcgMTYuMzcwOCAxMi4wNjY3IDE2LjI4MzMgMTIuMSAxNi4yTDE0LjM1IDEwLjlDMTQuNDgzMyAxMC42MTY3IDE0LjY3NSAxMC4zOTU4IDE0LjkyNSAxMC4yMzc1QzE1LjE3NSAxMC4wNzkyIDE1LjQ1IDEwIDE1Ljc1IDEwSDIyQzIyLjU1IDEwIDIzLjAyMDggMTAuMTk1OCAyMy40MTI1IDEwLjU4NzVDMjMuODA0MiAxMC45NzkyIDI0IDExLjQ1IDI0IDEyVjE4QzI0IDE4LjIgMjMuOTYyNSAxOC4zODc1IDIzLjg4NzUgMTguNTYyNUMyMy44MTI1IDE4LjczNzUgMjMuNyAxOC45IDIzLjU1IDE5LjA1TDE5LjQgMjMuMkMxOS4yNSAyMy4zNSAxOS4wODMzIDIzLjQ2MjUgMTguOSAyMy41Mzc1QzE4LjcxNjcgMjMuNjEyNSAxOC41MzMzIDIzLjY1IDE4LjM1IDIzLjY1QzE3LjkxNjcgMjMuNjUgMTcuNTQxNyAyMy40ODMzIDE3LjIyNSAyMy4xNUMxNi45MDgzIDIyLjgxNjcgMTYuNzkxNyAyMi4zOTE3IDE2Ljg3NSAyMS44NzVMMTguMiAxOUgxM1oiIGZpbGw9IiNCRENGREIiLz4KPC9nPgo8L2c+Cjwvc3ZnPgo=',
+ question:
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgaWQ9InBvbGwtaWNvbnMiPgo8bWFzayBpZD0ibWFzazBfOTYzN182NjYzMCIgc3R5bGU9Im1hc2stdHlwZTphbHBoYSIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeD0iMCIgeT0iMCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij4KPHJlY3QgaWQ9IkJvdW5kaW5nIGJveCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiBmaWxsPSIjRkY0MTREIi8+CjwvbWFzaz4KPGcgbWFzaz0idXJsKCNtYXNrMF85NjM3XzY2NjMwKSI+CjxwYXRoIGlkPSJWZWN0b3IiIGQ9Ik02LjI1IDEzLjg3NUgxMy43NVYxMi4zNzVINi4yNVYxMy44NzVaTTYuMjUgMTAuODc1SDE3Ljc1VjkuMzc1SDYuMjVWMTAuODc1Wk02LjI1IDcuODc1SDE3Ljc1VjYuMzc1SDYuMjVWNy44NzVaTTQuMzA3NzUgMTcuNzVDMy44MTA1OCAxNy43NSAzLjM4NSAxNy41NzMgMy4wMzEgMTcuMjE5QzIuNjc3IDE2Ljg2NSAyLjUgMTYuNDM5NCAyLjUgMTUuOTQyM1Y0LjMwNzc1QzIuNSAzLjgxMDU4IDIuNjc3IDMuMzg1IDMuMDMxIDMuMDMxQzMuMzg1IDIuNjc3IDMuODEwNTggMi41IDQuMzA3NzUgMi41SDE5LjY5MjNDMjAuMTg5NCAyLjUgMjAuNjE1IDIuNjc3IDIwLjk2OSAzLjAzMUMyMS4zMjMgMy4zODUgMjEuNSAzLjgxMDU4IDIxLjUgNC4zMDc3NVYxNS45NDIzQzIxLjUgMTYuNDM5NCAyMS4zMjMgMTYuODY1IDIwLjk2OSAxNy4yMTlDMjAuNjE1IDE3LjU3MyAyMC4xODk0IDE3Ljc1IDE5LjY5MjMgMTcuNzVIMTQuNDgyN0wxMi43NDggMjAuMzU1N0MxMi42NTczIDIwLjQ5MjkgMTIuNTQ3NyAyMC41OTU4IDEyLjQxOTIgMjAuNjY0NUMxMi4yOTA5IDIwLjczMyAxMi4xNTEyIDIwLjc2NzMgMTIgMjAuNzY3M0MxMS44NDg4IDIwLjc2NzMgMTEuNzA5MSAyMC43MzMgMTEuNTgwOCAyMC42NjQ1QzExLjQ1MjMgMjAuNTk1OCAxMS4zNDI3IDIwLjQ5MjkgMTEuMjUyIDIwLjM1NTdMOS41MTcyNSAxNy43NUg0LjMwNzc1WiIgZmlsbD0iI0JEQ0ZEQiIvPgo8L2c+CjwvZz4KPC9zdmc+Cg==',
+ 'bar-chart':
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggaWQ9ImJhcl9jaGFydCIgZD0iTTI2LjQ2MTMgMzJDMjUuOTE1MiAzMiAyNS40NTcyIDMxLjgxNTEgMjUuMDg3NSAzMS40NDUzQzI0LjcxOCAzMS4wNzU5IDI0LjUzMzMgMzAuNjE4IDI0LjUzMzMgMzAuMDcxNVYyMS4zMzMzQzI0LjUzMzMgMjAuNzg3MiAyNC43MTggMjAuMzI5MiAyNS4wODc1IDE5Ljk1OTVDMjUuNDU3MiAxOS41OSAyNS45MTUyIDE5LjQwNTMgMjYuNDYxMyAxOS40MDUzSDMwLjA3MTVDMzAuNjE4IDE5LjQwNTMgMzEuMDc1OSAxOS41OSAzMS40NDUzIDE5Ljk1OTVDMzEuODE1MSAyMC4zMjkyIDMyIDIwLjc4NzIgMzIgMjEuMzMzM1YzMC4wNzE1QzMyIDMwLjYxOCAzMS44MTUxIDMxLjA3NTkgMzEuNDQ1MyAzMS40NDUzQzMxLjA3NTkgMzEuODE1MSAzMC42MTggMzIgMzAuMDcxNSAzMkgyNi40NjEzWk0xNC4xOTQ3IDMyQzEzLjY0ODUgMzIgMTMuMTkwNiAzMS44MTUxIDEyLjgyMDggMzEuNDQ1M0MxMi40NTE0IDMxLjA3NTkgMTIuMjY2NyAzMC42MTggMTIuMjY2NyAzMC4wNzE1VjEuOTI4NTNDMTIuMjY2NyAxLjM4MjA0IDEyLjQ1MTQgMC45MjQwOSAxMi44MjA4IDAuNTU0NjY4QzEzLjE5MDYgMC4xODQ4OSAxMy42NDg1IDAgMTQuMTk0NyAwSDE3LjgwNTNDMTguMzUxNSAwIDE4LjgwOTQgMC4xODQ4OSAxOS4xNzkyIDAuNTU0NjY4QzE5LjU0ODYgMC45MjQwOSAxOS43MzMzIDEuMzgyMDQgMTkuNzMzMyAxLjkyODUzVjMwLjA3MTVDMTkuNzMzMyAzMC42MTggMTkuNTQ4NiAzMS4wNzU5IDE5LjE3OTIgMzEuNDQ1M0MxOC44MDk0IDMxLjgxNTEgMTguMzUxNSAzMiAxNy44MDUzIDMySDE0LjE5NDdaTTEuOTI4NTMgMzJDMS4zODIwNCAzMiAwLjkyNDA4OSAzMS44MTUxIDAuNTU0NjY2IDMxLjQ0NTNDMC4xODQ4ODkgMzEuMDc1OSAwIDMwLjYxOCAwIDMwLjA3MTVWMTIuMzk5NUMwIDExLjg0MzQgMC4xODQ4ODkgMTEuMzgxMyAwLjU1NDY2NiAxMS4wMTMzQzAuOTI0MDg5IDEwLjY0NTcgMS4zODIwNCAxMC40NjE5IDEuOTI4NTMgMTAuNDYxOUg1LjUzODY3QzYuMDg0OCAxMC40NjE5IDYuNTQyNzYgMTAuNjQ2NiA2LjkxMjUzIDExLjAxNkM3LjI4MTk2IDExLjM4NTQgNy40NjY2NyAxMS44NDM0IDcuNDY2NjcgMTIuMzg5OVYzMC4wNjE5QzcuNDY2NjcgMzAuNjE4MyA3LjI4MTk2IDMxLjA4MDQgNi45MTI1MyAzMS40NDhDNi41NDI3NiAzMS44MTYgNi4wODQ4IDMyIDUuNTM4NjcgMzJIMS45Mjg1M1oiIGZpbGw9IiMxRDFEMUQiLz4KPC9zdmc+Cg==',
+ anonymous:
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgaWQ9Ikljb24mIzYwO01lZGl1bSYjNjI7L0Fub255bW91cyI+CjxtYXNrIGlkPSJtYXNrMF85NjM3XzUzMzM5IiBzdHlsZT0ibWFzay10eXBlOmFscGhhIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSIwIiB5PSIwIiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiPgo8cmVjdCBpZD0iQm91bmRpbmcgYm94IiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIGZpbGw9IiNEOUQ5RDkiLz4KPC9tYXNrPgo8ZyBtYXNrPSJ1cmwoI21hc2swXzk2MzdfNTMzMzkpIj4KPHBhdGggaWQ9InZpc2liaWxpdHlfbG9jayIgZD0iTTEzLjgzMTcgMTYuNTkyNkMxMy43MTI4IDE2LjU5MjYgMTMuNjEzMSAxNi41NTIzIDEzLjUzMjYgMTYuNDcxOUMxMy40NTIxIDE2LjM5MTQgMTMuNDExOSAxNi4yOTE2IDEzLjQxMTkgMTYuMTcyOFYxMy4yOTNDMTMuNDExOSAxMy4xNzU1IDEzLjQ1NzggMTMuMDczOSAxMy41NDk2IDEyLjk4ODRDMTMuNjQxNiAxMi45MDMgMTMuNzQ5IDEyLjg2NzIgMTMuODcxOSAxMi44ODExSDE0LjM3MTlWMTEuODgxMUMxNC4zNzE5IDExLjUwMTYgMTQuNTA3NiAxMS4xNzY5IDE0Ljc3ODggMTAuOTA2N0MxNS4wNTAxIDEwLjYzNjQgMTUuMzc2MiAxMC41MDEzIDE1Ljc1NzEgMTAuNTAxM0MxNi4xMzgxIDEwLjUwMTMgMTYuNDYxIDEwLjYzNjQgMTYuNzI1OSAxMC45MDY3QzE2Ljk5MDkgMTEuMTc2OSAxNy4xMjM0IDExLjUwMTYgMTcuMTIzNCAxMS44ODExVjEyLjg4MTFIMTcuNjIzNEMxNy43NDkxIDEyLjg4MTEgMTcuODU3MSAxMi45MTk2IDE3Ljk0NzYgMTIuOTk2N0MxOC4wMzgxIDEzLjA3MzkgMTguMDgzNCAxMy4xNzI3IDE4LjA4MzQgMTMuMjkzVjE2LjE3MjhDMTguMDgzNCAxNi4yOTE2IDE4LjA0MzEgMTYuMzkxNCAxNy45NjI2IDE2LjQ3MTlDMTcuODgyMSAxNi41NTIzIDE3Ljc4MjQgMTYuNTkyNiAxNy42NjM0IDE2LjU5MjZIMTMuODMxN1pNMTUuMDI1NyAxMi44ODExSDE2LjQ2OTZWMTEuODgxMUMxNi40Njk2IDExLjY2ODYgMTYuNDAzNSAxMS40OTQ1IDE2LjI3MTMgMTEuMzU4OEMxNi4xMzkxIDExLjIyMyAxNS45Njc0IDExLjE1NTEgMTUuNzU2MSAxMS4xNTUxQzE1LjU0NDkgMTEuMTU1MSAxNS4zNzAzIDExLjIyMyAxNS4yMzI0IDExLjM1ODhDMTUuMDk0NiAxMS40OTQ1IDE1LjAyNTcgMTEuNjY4NiAxNS4wMjU3IDExLjg4MTFWMTIuODgxMVpNMTAuMDAwMSAxMi4wMDEzQzkuNDQ0NTEgMTIuMDAxMyA4Ljk3MjI4IDExLjgwNjkgOC41ODM0IDExLjQxOEM4LjE5NDUxIDExLjAyOTEgOC4wMDAwNiAxMC41NTY5IDguMDAwMDYgMTAuMDAxM0M4LjAwMDA2IDkuNDQ1NzUgOC4xOTQ1MSA4Ljk3MzUyIDguNTgzNCA4LjU4NDY0QzguOTcyMjggOC4xOTU3NSA5LjQ0NDUxIDguMDAxMyAxMC4wMDAxIDguMDAxM0MxMC41NTU2IDguMDAxMyAxMS4wMjc4IDguMTk1NzUgMTEuNDE2NyA4LjU4NDY0QzExLjgwNTYgOC45NzM1MiAxMi4wMDAxIDkuNDQ1NzUgMTIuMDAwMSAxMC4wMDEzQzEyLjAwMDEgMTAuNTU2OSAxMS44MDU2IDExLjAyOTEgMTEuNDE2NyAxMS40MThDMTEuMDI3OCAxMS44MDY5IDEwLjU1NTYgMTIuMDAxMyAxMC4wMDAxIDEyLjAwMTNaTTEwLjAwMDEgMTUuNTg0NkM4LjI4NjA0IDE1LjU4NDYgNi42ODM5NSAxNS4xMzg5IDUuMTkzODEgMTQuMjQ3M0MzLjcwMzUzIDEzLjM1NTcgMi41NjMxMiAxMi4xNDU2IDEuNzcyNTYgMTAuNjE3MUMxLjcxODEyIDEwLjUxOTggMS42NzgzMyAxMC40MjA5IDEuNjUzMTkgMTAuMzIwNUMxLjYyODA1IDEwLjIyMDEgMS42MTU0OCAxMC4xMTM3IDEuNjE1NDggMTAuMDAxM0MxLjYxNTQ4IDkuODg4OTQgMS42MjgwNSA5Ljc4MjU1IDEuNjUzMTkgOS42ODIxNEMxLjY3ODMzIDkuNTgxNzIgMS43MTgxMiA5LjQ4MjgzIDEuNzcyNTYgOS4zODU0N0MyLjU2MzEyIDcuODU3IDMuNzAzNTMgNi42NDY5MyA1LjE5MzgxIDUuNzU1MjZDNi42ODM5NSA0Ljg2MzczIDguMjg2MDQgNC40MTc5NyAxMC4wMDAxIDQuNDE3OTdDMTAuODg3NiA0LjQxNzk3IDExLjczOTUgNC41MzcxMyAxMi41NTU5IDQuNzc1NDdDMTMuMzcyMyA1LjAxMzY2IDE0LjEzNDEgNS4zNTAxOSAxNC44NDEzIDUuNzg1MDVDMTUuMzk1NSA2LjEyMTcyIDE1LjkwNzcgNi41MDk1NyAxNi4zNzggNi45NDg1OUMxNi44NDgzIDcuMzg3NjIgMTcuMjcyNSA3Ljg3NzQxIDE3LjY1MDcgOC40MTc5N0MxNy43OTI4IDguNjI3NDEgMTcuOCA4Ljg0NjQ0IDE3LjY3MjQgOS4wNzUwNUMxNy41NDQ3IDkuMzAzNjYgMTcuMzUzMiA5LjQxNzk3IDE3LjA5NzggOS40MTc5N0gxNS43NTE3QzE1LjIzODcgOS40MTc5NyAxNC43NjE5IDkuNDg2MzcgMTQuMzIxMyA5LjYyMzE4QzEzLjg4MDYgOS43NTk4NCAxMy40ODQgOS45NjQ5OCAxMy4xMzE1IDEwLjIzODZDMTMuMTM2OCAxMC4xOTQ3IDEzLjE0MDggMTAuMTUzNyAxMy4xNDM0IDEwLjExNTdDMTMuMTQ2MiAxMC4wNzc2IDEzLjE0NzYgMTAuMDM5NSAxMy4xNDc2IDEwLjAwMTNDMTMuMTQ3NiA5LjEyNyAxMi44NDE0IDguMzgzODcgMTIuMjI5IDcuNzcxOTNDMTEuNjE2NyA3LjE1OTg0IDEwLjg3MyA2Ljg1MzggOS45OTgxOSA2Ljg1MzhDOS4xMjMzMyA2Ljg1MzggOC4zODAzNCA3LjE1OTk4IDcuNzY5MjMgNy43NzIzNEM3LjE1ODEyIDguMzg0NzEgNi44NTI1NiA5LjEyODMyIDYuODUyNTYgMTAuMDAzMkM2Ljg1MjU2IDEwLjg3OCA3LjE1ODYgMTEuNjIxIDcuNzcwNjkgMTIuMjMyMUM4LjM4MjYzIDEyLjg0MzIgOS4xMjU3NiAxMy4xNDg4IDEwLjAwMDEgMTMuMTQ4OEMxMC4zNjEyIDEzLjE0ODggMTAuNzEgMTMuMDkwNSAxMS4wNDY1IDEyLjk3NEMxMS4zODMgMTIuODU3NiAxMS42ODQ5IDEyLjY5NjkgMTEuOTUxOSAxMi40OTE3QzExLjk0MTIgMTIuNTIzNyAxMS45MzE5IDEyLjU3MTggMTEuOTI0IDEyLjYzNjFDMTEuOTE2IDEyLjcwMDMgMTEuOTExOSAxMi43NjU5IDExLjkxMTkgMTIuODMzVjE0Ljc5MjZDMTEuOTExOSAxNC45NzAzIDExLjg1NjkgMTUuMTI3MiAxMS43NDY3IDE1LjI2MzJDMTEuNjM2NiAxNS4zOTkzIDExLjQ5MzUgMTUuNDc4NyAxMS4zMTc0IDE1LjUwMTNDMTEuMTAyNSAxNS41MjkxIDEwLjg4NzYgMTUuNTQ5OSAxMC42NzI2IDE1LjU2MzhDMTAuNDU3NyAxNS41Nzc3IDEwLjIzMzUgMTUuNTg0NiAxMC4wMDAxIDE1LjU4NDZaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjwvZz4KPC9zdmc+Cg==',
+ 'stop-watch':
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgaWQ9Ikljb24mIzYwO01lZGl1bSYjNjI7L1N0b3BfV2F0Y2giPgo8bWFzayBpZD0ibWFzazBfOTYzN181MzMwNiIgc3R5bGU9Im1hc2stdHlwZTphbHBoYSIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeD0iMCIgeT0iMCIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIj4KPHJlY3QgaWQ9IkJvdW5kaW5nIGJveCIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIiBmaWxsPSIjRDlEOUQ5Ii8+CjwvbWFzaz4KPGcgbWFzaz0idXJsKCNtYXNrMF85NjM3XzUzMzA2KSI+CjxwYXRoIGlkPSJhbGFybV9vbiIgZD0iTTkuMTQ1NzkgMTEuNTk1M0w3Ljg4OTMzIDEwLjMzODlDNy43NTQwNiAxMC4yMDExIDcuNTk1NzkgMTAuMTMwOCA3LjQxNDU0IDEwLjEyOEM3LjIzMzI5IDEwLjEyNTQgNy4wNzEwNyAxMC4xOTM1IDYuOTI3ODcgMTAuMzMyNEM2Ljc5ODU3IDEwLjQ3MTQgNi43MzM5MiAxMC42MzE0IDYuNzMzOTIgMTAuODEyNEM2LjczMzkyIDEwLjk5MzQgNi43OTg1NyAxMS4xNTM0IDYuOTI3ODcgMTEuMjkyNEw4LjY4MSAxMy4wNTg0QzguODE2NTYgMTMuMTk1MSA4Ljk3NDYxIDEzLjI2MzQgOS4xNTUxNyAxMy4yNjM0QzkuMzM1ODYgMTMuMjYzNCA5LjQ5NDYxIDEzLjE5NTEgOS42MzE0MiAxMy4wNTg0TDEzLjEwNTggOS41ODQwNkMxMy4yNDM2IDkuNDQ4OTIgMTMuMzEzOCA5LjI4ODUxIDEzLjMxNjQgOS4xMDI4MUMxMy4zMTkxIDguOTE3MjYgMTMuMjQ5OSA4Ljc1Mjg4IDEzLjEwODkgOC42MDk2OUMxMi45Njc5IDguNDY2NDkgMTIuODA1NiA4LjM5NDkgMTIuNjIxOCA4LjM5NDlDMTIuNDM4MSA4LjM5NDkgMTIuMjc0NiA4LjQ2NjQ5IDEyLjEzMTQgOC42MDk2OUw5LjE0NTc5IDExLjU5NTNaTTkuOTk5OTYgMTcuNTgyNEM5LjA4NjQ5IDE3LjU4MjQgOC4yMzA1OCAxNy40MDk3IDcuNDMyMjUgMTcuMDY0M0M2LjYzMzkyIDE2LjcxODkgNS45Mzc4MSAxNi4yNDkxIDUuMzQzOTIgMTUuNjU1MUM0Ljc0OTg5IDE1LjA2MTIgNC4yODAxNyAxNC4zNjUxIDMuOTM0NzUgMTMuNTY2OEMzLjU4OTMzIDEyLjc2ODQgMy40MTY2MiAxMS45MTI1IDMuNDE2NjIgMTAuOTk5MUMzLjQxNjYyIDEwLjA4NTYgMy41ODkzMyA5LjIyOTY5IDMuOTM0NzUgOC40MzEzNUM0LjI4MDE3IDcuNjMzMDIgNC43NDk4OSA2LjkzNjkxIDUuMzQzOTIgNi4zNDMwMkM1LjkzNzgxIDUuNzQ4OTkgNi42MzM5MiA1LjI3OTI3IDcuNDMyMjUgNC45MzM4NUM4LjIzMDU4IDQuNTg4NDQgOS4wODY0OSA0LjQxNTczIDkuOTk5OTYgNC40MTU3M0MxMC45MTM0IDQuNDE1NzMgMTEuNzY5MyA0LjU4ODQ0IDEyLjU2NzcgNC45MzM4NUMxMy4zNjYgNS4yNzkyNyAxNC4wNjIxIDUuNzQ4OTkgMTQuNjU2IDYuMzQzMDJDMTUuMjUgNi45MzY5MSAxNS43MTk3IDcuNjMzMDIgMTYuMDY1MiA4LjQzMTM1QzE2LjQxMDYgOS4yMjk2OSAxNi41ODMzIDEwLjA4NTYgMTYuNTgzMyAxMC45OTkxQzE2LjU4MzMgMTEuOTEyNSAxNi40MTA2IDEyLjc2ODQgMTYuMDY1MiAxMy41NjY4QzE1LjcxOTcgMTQuMzY1MSAxNS4yNSAxNS4wNjEyIDE0LjY1NiAxNS42NTUxQzE0LjA2MjEgMTYuMjQ5MSAxMy4zNjYgMTYuNzE4OSAxMi41Njc3IDE3LjA2NDNDMTEuNzY5MyAxNy40MDk3IDEwLjkxMzQgMTcuNTgyNCA5Ljk5OTk2IDE3LjU4MjRaTTIuMjQyMDQgNi40NDEzNUMyLjEyMTIxIDYuMzIwNjYgMi4wNjA3OSA2LjE5MTYzIDIuMDYwNzkgNi4wNTQyN0MyLjA2MDc5IDUuOTE3MDUgMi4xMjEyMSA1Ljc4ODA5IDIuMjQyMDQgNS42Njc0TDQuNjg5MTIgMy4yMjAzMUM0LjgwNDU0IDMuMTA0OSA0LjkzMjE4IDMuMDQ1ODcgNS4wNzIwNCAzLjA0MzIzQzUuMjEyMDQgMy4wNDA0NSA1LjM0MjM5IDMuMDk5NDggNS40NjMwOCAzLjIyMDMxQzUuNTgzNzggMy4zNDEwMSA1LjY0NDEyIDMuNDY5OTcgNS42NDQxMiAzLjYwNzE5QzUuNjQ0MTIgMy43NDQ1NSA1LjU4Mzc4IDMuODczNTggNS40NjMwOCAzLjk5NDI3TDIuOTk1MTcgNi40NjIxOUMyLjg3OTc1IDYuNTc3NiAyLjc1NTU4IDYuNjMzMTYgMi42MjI2NyA2LjYyODg1QzIuNDg5NjEgNi42MjQ1NSAyLjM2Mjc0IDYuNTYyMDUgMi4yNDIwNCA2LjQ0MTM1Wk0xNy43Nzg3IDYuNDQxMzVDMTcuNjU4IDYuNTYyMDUgMTcuNTI5MSA2LjYyMjQgMTcuMzkxOCA2LjYyMjRDMTcuMjU0NSA2LjYyMjQgMTcuMTI1NCA2LjU2MjA1IDE3LjAwNDcgNi40NDEzNUwxNC41NTc3IDMuOTk0MjdDMTQuNDQyMyAzLjg3ODg1IDE0LjM4MzIgMy43NTEyMiAxNC4zODA2IDMuNjExMzVDMTQuMzc3OSAzLjQ3MTM1IDE0LjQzNyAzLjM0MTAxIDE0LjU1NzcgMy4yMjAzMUMxNC42Nzg0IDMuMDk5NDggMTQuODA3NCAzLjAzOTA2IDE0Ljk0NDcgMy4wMzkwNkMxNS4wODIgMy4wMzkwNiAxNS4yMTA5IDMuMDk5NDggMTUuMzMxNiAzLjIyMDMxTDE3Ljc3ODcgNS42ODgyM0MxNy44OTQxIDUuODAzNjUgMTcuOTUzMiA1LjkyNzgxIDE3Ljk1NTggNi4wNjA3M0MxNy45NTg2IDYuMTkzNzkgMTcuODk5NSA2LjMyMDY2IDE3Ljc3ODcgNi40NDEzNVoiIGZpbGw9IndoaXRlIi8+CjwvZz4KPC9nPgo8L3N2Zz4K',
+ group:
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgaWQ9Ikljb24mIzYwO01lZGl1bSYjNjI7L0F0dGVuZGVlIj4KPG1hc2sgaWQ9Im1hc2swXzk2MzdfNTMzMTciIHN0eWxlPSJtYXNrLXR5cGU6YWxwaGEiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjAiIHk9IjAiIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCI+CjxyZWN0IGlkPSJCb3VuZGluZyBib3giIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI0Q5RDlEOSIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazBfOTYzN181MzMxNykiPgo8cGF0aCBpZD0iZ3JvdXBzIiBkPSJNMi4wNTQ2NiAxNC4zMjM4QzEuODc4MTQgMTQuMzIzOCAxLjcyNzY1IDE0LjI2MTYgMS42MDMyMSAxNC4xMzczQzEuNDc4OSAxNC4wMTI5IDEuNDE2NzUgMTMuODYyNCAxLjQxNjc1IDEzLjY4NTlWMTMuNDMyOEMxLjQxNjc1IDEyLjkzMzkgMS42NzE1NCAxMi41MjcxIDIuMTgxMTIgMTIuMjEyM0MyLjY5MDg1IDExLjg5NzggMy4zNjkzMiAxMS43NDA1IDQuMjE2NTQgMTEuNzQwNUM0LjM4NDE4IDExLjc0MDUgNC41NDU5OCAxMS43NDg3IDQuNzAxOTYgMTEuNzY1M0M0Ljg1NzkzIDExLjc4MTggNS4wMTUwMSAxMS44MDM5IDUuMTczMjEgMTEuODMxN0M1LjAzODYyIDEyLjA0OTggNC45MzU1IDEyLjI4NTMgNC44NjM4MyAxMi41Mzg0QzQuNzkyMyAxMi43OTE2IDQuNzU2NTQgMTMuMDQyNyA0Ljc1NjU0IDEzLjI5MTdWMTQuMzIzOEgyLjA1NDY2Wk02LjU4OTg3IDE0LjMyMzhDNi40MDA3MSAxNC4zMjM4IDYuMjQxMjYgMTQuMjU4NSA2LjExMTU0IDE0LjEyOEM1Ljk4MTY4IDEzLjk5NzYgNS45MTY3NSAxMy44Mzk1IDUuOTE2NzUgMTMuNjUzOFYxMy4zMjM4QzUuOTE2NzUgMTIuOTg5MiA2LjAwNTQzIDEyLjY5MTYgNi4xODI3OSAxMi40MzExQzYuMzYwMTUgMTIuMTcwNCA2LjYzNDE4IDExLjk0MDcgNy4wMDQ4NyAxMS43NDIxQzcuMzc1NTcgMTEuNTQzNCA3LjgxMDcxIDExLjM5NzggOC4zMTAyOSAxMS4zMDU1QzguODA5NzMgMTEuMjEzIDkuMzc1MzYgMTEuMTY2NyAxMC4wMDcyIDExLjE2NjdDMTAuNjM4MSAxMS4xNjY3IDExLjE5OTkgMTEuMjEzIDExLjY5MjQgMTEuMzA1NUMxMi4xODQ5IDExLjM5NzggMTIuNjE2NSAxMS41NDM0IDEyLjk4NzIgMTEuNzQyMUMxMy4zNTggMTEuOTI2OSAxMy42MzM0IDEyLjE1NDMgMTMuODEzNCAxMi40MjQ0QzEzLjk5MzQgMTIuNjk0NiAxNC4wODM0IDEyLjk5NDQgMTQuMDgzNCAxMy4zMjM4VjEzLjY1MzhDMTQuMDgzNCAxMy44Mzk1IDE0LjAxODEgMTMuOTk3NiAxMy44ODc2IDE0LjEyOEMxMy43NTcyIDE0LjI1ODUgMTMuNTk5MSAxNC4zMjM4IDEzLjQxMzQgMTQuMzIzOEg2LjU4OTg3Wk0xNS4yNDM2IDE0LjMyMzhWMTMuMjkyOEMxNS4yNDM2IDEzLjAzNDYgMTUuMjA2NSAxMi43ODQ1IDE1LjEzMjQgMTIuNTQyNkMxNS4wNTgxIDEyLjMwMDYgMTQuOTUzNiAxMi4wNjM3IDE0LjgxOSAxMS44MzE3QzE0Ljk1NDcgMTEuODAzOSAxNS4wOTcxIDExLjc4MTggMTUuMjQ2MSAxMS43NjUzQzE1LjM5NTIgMTEuNzQ4NyAxNS41NzcgMTEuNzQwNSAxNS43OTE3IDExLjc0MDVDMTYuNjM5IDExLjc0MDUgMTcuMzE2MSAxMS44OTkxIDE3LjgyMyAxMi4yMTY1QzE4LjMyOTkgMTIuNTMzNyAxOC41ODM0IDEyLjkzOTEgMTguNTgzNCAxMy40MzI4VjEzLjY4NTlDMTguNTgzNCAxMy44NjI0IDE4LjUyMTMgMTQuMDEyOSAxOC4zOTcgMTQuMTM3M0MxOC4yNzI1IDE0LjI2MTYgMTguMTIyIDE0LjMyMzggMTcuOTQ1NSAxNC4zMjM4SDE1LjI0MzZaTTQuMjE0MjUgMTAuNzY0NEMzLjg2MzE0IDEwLjc2NDQgMy41NjU3OCAxMC42NDIxIDMuMzIyMTYgMTAuMzk3NkMzLjA3ODU1IDEwLjE1MyAyLjk1Njc1IDkuODU1NCAyLjk1Njc1IDkuNTA0ODRDMi45NTY3NSA5LjE2MzA0IDMuMDc5MTEgOC44NjkwMSAzLjMyMzgzIDguNjIyNzZDMy41Njg0MSA4LjM3NjUxIDMuODY1OTggOC4yNTMzOCA0LjIxNjU0IDguMjUzMzhDNC41NTgzNSA4LjI1MzM4IDQuODUzNjkgOC4zNzY1MSA1LjEwMjU4IDguNjIyNzZDNS4zNTE2MSA4Ljg2OTAxIDUuNDc2MTIgOS4xNjM4NyA1LjQ3NjEyIDkuNTA3MzRDNS40NzYxMiA5Ljg1MjkgNS4zNTMwNyAxMC4xNDg4IDUuMTA2OTYgMTAuMzk1MUM0Ljg2MDk4IDEwLjY0MTMgNC41NjM0MSAxMC43NjQ0IDQuMjE0MjUgMTAuNzY0NFpNMTUuNzkxNyAxMC43NjQ0QzE1LjQ0NDUgMTAuNzY0NCAxNS4xNDc4IDEwLjY0MTMgMTQuOTAxNSAxMC4zOTUxQzE0LjY1NTMgMTAuMTQ4OCAxNC41MzIyIDkuODUyOSAxNC41MzIyIDkuNTA3MzRDMTQuNTMyMiA5LjE2Mzg3IDE0LjY1NTMgOC44NjkwMSAxNC45MDE1IDguNjIyNzZDMTUuMTQ3OCA4LjM3NjUxIDE1LjQ0NSA4LjI1MzM4IDE1Ljc5MzIgOC4yNTMzOEMxNi4xMzk1IDguMjUzMzggMTYuNDM1NyA4LjM3NjUxIDE2LjY4MiA4LjYyMjc2QzE2LjkyODIgOC44NjkwMSAxNy4wNTEzIDkuMTYzMDQgMTcuMDUxMyA5LjUwNDg0QzE3LjA1MTMgOS44NTU0IDE2LjkyODYgMTAuMTUzIDE2LjY4MyAxMC4zOTc2QzE2LjQzNzQgMTAuNjQyMSAxNi4xNDA0IDEwLjc2NDQgMTUuNzkxNyAxMC43NjQ0Wk0xMC4wMDMgMTAuMTY2N0M5LjQ3MjE3IDEwLjE2NjcgOS4wMjAwOCA5Ljk4MDU0IDguNjQ2NzUgOS42MDgxOEM4LjI3MzQyIDkuMjM1ODIgOC4wODY3NSA4Ljc4MzczIDguMDg2NzUgOC4yNTE5M0M4LjA4Njc1IDcuNzIzMTggOC4yNzI4NiA3LjI3MjM0IDguNjQ1MDggNi44OTk0M0M5LjAxNzQ0IDYuNTI2MzcgOS40Njk2IDYuMzM5ODQgMTAuMDAxNSA2LjMzOTg0QzEwLjUzMDIgNi4zMzk4NCAxMC45ODEgNi41MjY0NCAxMS4zNTQgNi44OTk2NEMxMS43MjcgNy4yNzI2OSAxMS45MTM0IDcuNzIyOTcgMTEuOTEzNCA4LjI1MDQ3QzExLjkxMzQgOC43ODExNiAxMS43MjY5IDkuMjMzMjUgMTEuMzUzOCA5LjYwNjcyQzEwLjk4MDYgOS45ODAwNSAxMC41MzA0IDEwLjE2NjcgMTAuMDAzIDEwLjE2NjdaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjwvZz4KPC9zdmc+Cg==',
+ 'co-host':
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgaWQ9Ikljb24mIzYwO01lZGl1bSYjNjI7L0NvLUhvc3QiPgo8bWFzayBpZD0ibWFzazBfOTYzN181MzMyOCIgc3R5bGU9Im1hc2stdHlwZTphbHBoYSIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeD0iMCIgeT0iMCIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIj4KPHJlY3QgaWQ9IkJvdW5kaW5nIGJveCIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIiBmaWxsPSIjRDlEOUQ5Ii8+CjwvbWFzaz4KPGcgbWFzaz0idXJsKCNtYXNrMF85NjM3XzUzMzI4KSI+CjxwYXRoIGlkPSJncm91cCIgZD0iTTIuNjY1MjggMTMuOTg2N0MyLjY2NTI4IDEzLjY2ODQgMi43NDEzOSAxMy4zODkzIDIuODkzNjIgMTMuMTQ5NEMzLjA0NTg0IDEyLjkwOTYgMy4yNTg1NSAxMi43MDk4IDMuNTMxNzQgMTIuNTUwM0M0LjIxMjU3IDEyLjE0OSA0LjkyOTAzIDExLjgzMyA1LjY4MTEyIDExLjYwMjFDNi40MzMzNCAxMS4zNzE0IDcuMjg5MSAxMS4yNTYxIDguMjQ4NDEgMTEuMjU2MUM5LjIwNzg1IDExLjI1NjEgMTAuMDYzNiAxMS4zNzE0IDEwLjgxNTcgMTEuNjAyMUMxMS41Njc5IDExLjgzMyAxMi4yODQ1IDEyLjE0OSAxMi45NjUzIDEyLjU1MDNDMTMuMjM4NSAxMi43MDk4IDEzLjQ1MTIgMTIuOTA5NiAxMy42MDM0IDEzLjE0OTRDMTMuNzU1NiAxMy4zODkzIDEzLjgzMTcgMTMuNjY4NCAxMy44MzE3IDEzLjk4NjdWMTQuMzIzNEMxMy44MzE3IDE0LjYxNzEgMTMuNzI2IDE0Ljg3MzggMTMuNTE0NSAxNS4wOTM0QzEzLjMwMjkgMTUuMzEzIDEzLjA0MjIgMTUuNDIyOCAxMi43MzI0IDE1LjQyMjhIMy43NjQ0NUMzLjQ1NDczIDE1LjQyMjggMy4xOTQxIDE1LjMxNyAyLjk4MjU3IDE1LjEwNTVDMi43NzEwNSAxNC44OTM4IDIuNjY1MjggMTQuNjMzMSAyLjY2NTI4IDE0LjMyMzRWMTMuOTg2N1pNMTUuMDcyMiAxNS40MjI4QzE1LjE0OTEgMTUuMjQ1NCAxNS4yMTE2IDE1LjA2NjIgMTUuMjU5NyAxNC44ODUxQzE1LjMwNzcgMTQuNzAzOSAxNS4zMzE3IDE0LjUxNjcgMTUuMzMxNyAxNC4zMjM0VjE0LjA1MDlDMTUuMzMxNyAxMy41MzE3IDE1LjIzMjYgMTMuMDU1NSAxNS4wMzQyIDEyLjYyMjNDMTQuODM1OCAxMi4xODkzIDE0LjU3MzggMTEuODM3IDE0LjI0ODQgMTEuNTY1NUMxNC42NDA1IDExLjY3NjYgMTUuMDI4NiAxMS44MTI1IDE1LjQxMjggMTEuOTczMkMxNS43OTY4IDEyLjEzNCAxNi4xNzUzIDEyLjMyNjYgMTYuNTQ4MiAxMi41NTA5QzE2Ljc4NDMgMTIuNjgzNCAxNi45NzQ1IDEyLjg4NDcgMTcuMTE4NiAxMy4xNTQ4QzE3LjI2MjggMTMuNDI0OCAxNy4zMzQ5IDEzLjcyMzUgMTcuMzM0OSAxNC4wNTA5VjE0LjMyMzRDMTcuMzM0OSAxNC42MzMxIDE3LjIyOTEgMTQuODkzOCAxNy4wMTc2IDE1LjEwNTVDMTYuODA2IDE1LjMxNyAxNi41NDU0IDE1LjQyMjggMTYuMjM1NyAxNS40MjI4SDE1LjA3MjJaTTguMjQ4NDEgOS43NDMxOEM3LjUyOTY2IDkuNzQzMTggNi45MTk0NSA5LjQ5MjM0IDYuNDE3NzggOC45OTA2OEM1LjkxNjEyIDguNDg4ODcgNS42NjUyOCA3Ljg3ODU5IDUuNjY1MjggNy4xNTk4NEM1LjY2NTI4IDYuNDQxMDkgNS45MTYxMiA1LjgzMDg5IDYuNDE3NzggNS4zMjkyMkM2LjkxOTQ1IDQuODI3NDEgNy41Mjk2NiA0LjU3NjUxIDguMjQ4NDEgNC41NzY1MUM4Ljk2NzE2IDQuNTc2NTEgOS41Nzc0NCA0LjgyNzQxIDEwLjA3OTIgNS4zMjkyMkMxMC41ODA5IDUuODMwODkgMTAuODMxNyA2LjQ0MTA5IDEwLjgzMTcgNy4xNTk4NEMxMC44MzE3IDcuODc4NTkgMTAuNTgwOSA4LjQ4ODg3IDEwLjA3OTIgOC45OTA2OEM5LjU3NzQ0IDkuNDkyMzQgOC45NjcxNiA5Ljc0MzE4IDguMjQ4NDEgOS43NDMxOFpNMTQuMTEwNSA3LjE1OTg0QzE0LjExMDUgNy44Nzg1OSAxMy44NTk3IDguNDg4ODcgMTMuMzU4IDguOTkwNjhDMTIuODU2MyA5LjQ5MjM0IDEyLjI0NjEgOS43NDMxOCAxMS41Mjc0IDkuNzQzMThDMTEuNDc1IDkuNzQzMTggMTEuNDQ1NiA5Ljc0NjM3IDExLjQzOTIgOS43NTI3NkMxMS40MzI3IDkuNzU5MTUgMTEuNDAzMyA5Ljc1NjUxIDExLjM1MTEgOS43NDQ4NEMxMS42NTE5IDkuMzkxMzcgMTEuODkwNiA4Ljk5ODE4IDEyLjA2NzIgOC41NjUyNkMxMi4yNDM1IDguMTMyMzQgMTIuMzMxNyA3LjY2MzU5IDEyLjMzMTcgNy4xNTkwMUMxMi4zMzE3IDYuNjU0MjkgMTIuMjQxNyA2LjE4NzQxIDEyLjA2MTcgNS43NTgzOUMxMS44ODE3IDUuMzI5NSAxMS42NDQ5IDQuOTM1MDUgMTEuMzUxMSA0LjU3NTA1QzExLjM4NzQgNC41NzM5NCAxMS40MTY3IDQuNTczOTQgMTEuNDM5MiA0LjU3NTA1QzExLjQ2MTYgNC41NzYwMiAxMS40OTEgNC41NzY1MSAxMS41Mjc0IDQuNTc2NTFDMTIuMjQ2MSA0LjU3NjUxIDEyLjg1NjMgNC44Mjc0MSAxMy4zNTggNS4zMjkyMkMxMy44NTk3IDUuODMwODkgMTQuMTEwNSA2LjQ0MTA5IDE0LjExMDUgNy4xNTk4NFoiIGZpbGw9IndoaXRlIi8+CjwvZz4KPC9nPgo8L3N2Zz4K',
+};
+
+export default pollIcons;
+export type {PollIconsInterface};
diff --git a/polling/ui/BaseAccordian.tsx b/polling/ui/BaseAccordian.tsx
new file mode 100644
index 0000000..f8f8597
--- /dev/null
+++ b/polling/ui/BaseAccordian.tsx
@@ -0,0 +1,157 @@
+import React, {useState, ReactNode, useEffect} from 'react';
+import {
+ View,
+ Text,
+ TouchableOpacity,
+ LayoutAnimation,
+ UIManager,
+ Platform,
+ StyleSheet,
+} from 'react-native';
+import {ThemeConfig, $config, ImageIcon} from 'customization-api';
+
+// Enable Layout Animation for Android
+if (Platform.OS === 'android') {
+ UIManager.setLayoutAnimationEnabledExperimental &&
+ UIManager.setLayoutAnimationEnabledExperimental(true);
+}
+
+// TypeScript Interfaces
+interface BaseAccordionProps {
+ children: ReactNode;
+}
+
+interface BaseAccordionHeaderProps {
+ title: string;
+ expandIcon?: React.ReactNode;
+ id: string;
+ isOpen: boolean; // Pass this prop explicitly
+ onPress: () => void; // Handle toggle functionality
+ children: React.ReactNode;
+}
+
+interface BaseAccordionContentProps {
+ children: ReactNode;
+}
+
+// Main Accordion Component to render multiple AccordionItems
+const BaseAccordion: React.FC = ({children}) => {
+ return {children};
+};
+
+// AccordionItem Component to manage isOpen state
+const BaseAccordionItem: React.FC<{children: ReactNode; open?: boolean}> = ({
+ children,
+ open = false,
+}) => {
+ const [isOpen, setIsOpen] = useState(open);
+
+ useEffect(() => {
+ setIsOpen(open);
+ }, [open]);
+
+ const toggleAccordion = () => {
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
+ setIsOpen(!isOpen);
+ };
+
+ // Separate AccordionHeader and AccordionContent components from children
+ const header = React.Children.toArray(children).find(
+ (child: any) => child.type === BaseAccordionHeader,
+ );
+ const content = React.Children.toArray(children).find(
+ (child: any) => child.type === BaseAccordionContent,
+ );
+
+ return (
+
+ {/* Clone and pass props to AccordionHeader */}
+ {header &&
+ React.cloneElement(header as React.ReactElement, {
+ isOpen, // Pass the isOpen state
+ onPress: toggleAccordion, // Pass the toggleAccordion function
+ })}
+ {isOpen && content}
+
+ );
+};
+
+// AccordionHeader Component for the Accordion Header
+const BaseAccordionHeader: React.FC> = ({
+ title,
+ isOpen,
+ onPress,
+ children,
+}) => {
+ return (
+
+
+ {title}
+ {children && {children}}
+
+
+
+
+
+ );
+};
+
+// AccordionContent Component for the Accordion Content
+const BaseAccordionContent: React.FC = ({
+ children,
+}) => {
+ return {children};
+};
+
+export {
+ BaseAccordion,
+ BaseAccordionItem,
+ BaseAccordionHeader,
+ BaseAccordionContent,
+};
+
+// Styles for Accordion Components
+const styles = StyleSheet.create({
+ accordionContainer: {
+ // marginVertical: 10,
+ },
+ accordionItem: {
+ marginBottom: 8,
+ borderRadius: 8,
+ },
+ accordionHeader: {
+ paddingVertical: 8,
+ paddingHorizontal: 19,
+ backgroundColor: $config.CARD_LAYER_3_COLOR,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ height: 36,
+ },
+ headerContent: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ expandIcon: {
+ marginLeft: 8,
+ },
+ accordionContent: {
+ paddingVertical: 20,
+ paddingHorizontal: 12,
+ backgroundColor: $config.CARD_LAYER_1_COLOR,
+ borderBottomLeftRadius: 8,
+ borderBottomRightRadius: 8,
+ },
+ accordionTitle: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.tiny,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: '700',
+ lineHeight: 12,
+ },
+});
diff --git a/polling/ui/BaseButtonWithToggle.tsx b/polling/ui/BaseButtonWithToggle.tsx
new file mode 100644
index 0000000..3e5d9ba
--- /dev/null
+++ b/polling/ui/BaseButtonWithToggle.tsx
@@ -0,0 +1,104 @@
+import {StyleSheet, Text, View, TouchableOpacity} from 'react-native';
+import React from 'react';
+import {
+ ThemeConfig,
+ $config,
+ ImageIcon,
+ hexadecimalTransparency,
+} from 'customization-api';
+import Toggle from '../../../src/atoms/Toggle';
+// import Tooltip from '../../../src/atoms/Tooltip';
+import PlatformWrapper from '../../../src/utils/PlatformWrapper';
+
+interface Props {
+ text: string;
+ value: boolean;
+ onPress: (value: boolean) => void;
+ tooltip?: boolean;
+ tooltTipText?: string;
+ hoverEffect?: boolean;
+ icon?: string;
+}
+
+const BaseButtonWithToggle = ({
+ text,
+ value,
+ onPress,
+ hoverEffect = false,
+ icon,
+}: Props) => {
+ return (
+
+ {/* {
+ return ( */}
+
+ {(isHovered: boolean) => {
+ return (
+ {
+ onPress(value);
+ }}>
+
+
+ {text}
+
+
+ {
+ onPress(toggle);
+ }}
+ />
+
+
+ );
+ }}
+
+ {/* );
+ }}
+ /> */}
+
+ );
+};
+
+export default BaseButtonWithToggle;
+
+const styles = StyleSheet.create({
+ toggleButton: {
+ width: '100%',
+ },
+ container: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: 8,
+ borderRadius: 4,
+ },
+ hover: {
+ backgroundColor: $config.SEMANTIC_NEUTRAL + hexadecimalTransparency['25%'],
+ },
+ text: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.tiny,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 12,
+ fontWeight: '400',
+ marginRight: 12,
+ },
+ centerRow: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+});
diff --git a/polling/ui/BaseModal.tsx b/polling/ui/BaseModal.tsx
new file mode 100644
index 0000000..9ea9b1f
--- /dev/null
+++ b/polling/ui/BaseModal.tsx
@@ -0,0 +1,217 @@
+import {
+ Modal,
+ View,
+ StyleSheet,
+ Text,
+ TouchableWithoutFeedback,
+ ScrollView,
+} from 'react-native';
+import React, {ReactNode} from 'react';
+import {
+ ThemeConfig,
+ hexadecimalTransparency,
+ IconButton,
+ isMobileUA,
+ $config,
+} from 'customization-api';
+
+interface TitleProps {
+ title?: string;
+ children?: ReactNode | ReactNode[];
+}
+
+function BaseModalTitle({title, children}: TitleProps) {
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+ {children}
+
+ );
+}
+
+interface ContentProps {
+ children: ReactNode;
+ noPadding?: boolean;
+}
+
+function BaseModalContent({children, noPadding}: ContentProps) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+interface ActionProps {
+ children: ReactNode;
+ alignRight?: boolean;
+}
+function BaseModalActions({children, alignRight}: ActionProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+type BaseModalProps = {
+ visible?: boolean;
+ onClose: () => void;
+ children: ReactNode;
+ width?: number;
+ cancelable?: boolean;
+};
+
+const BaseModal = ({
+ children,
+ visible = false,
+ width = 650,
+ cancelable = false,
+ onClose,
+}: BaseModalProps) => {
+ return (
+
+
+ {
+ cancelable && onClose();
+ }}>
+
+
+
+ {children}
+
+
+
+ );
+};
+
+type BaseModalCloseIconProps = {
+ onClose: () => void;
+};
+
+const BaseModalCloseIcon = ({onClose}: BaseModalCloseIconProps) => {
+ return (
+
+
+
+ );
+};
+export {
+ BaseModal,
+ BaseModalTitle,
+ BaseModalContent,
+ BaseModalActions,
+ BaseModalCloseIcon,
+};
+
+const style = StyleSheet.create({
+ baseModalContainer: {
+ flex: 1,
+ position: 'relative',
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ },
+ baseModal: {
+ zIndex: 2,
+ backgroundColor: $config.CARD_LAYER_1_COLOR,
+ borderWidth: 1,
+ borderColor: $config.CARD_LAYER_3_COLOR,
+ borderRadius: ThemeConfig.BorderRadius.large,
+ shadowColor: $config.HARD_CODED_BLACK_COLOR,
+ shadowOffset: {
+ width: 0,
+ height: 2,
+ },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 5,
+ minWidth: 340,
+ maxWidth: '90%',
+ minHeight: 220,
+ maxHeight: '80%', // Set a maximum height for the modal
+ overflow: 'hidden',
+ },
+ baseModalBody: {
+ flex: 1,
+ },
+ baseBackdrop: {
+ zIndex: 1,
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ backgroundColor:
+ $config.HARD_CODED_BLACK_COLOR + hexadecimalTransparency['60%'],
+ },
+ scrollgrow: {
+ flexGrow: 1,
+ },
+ header: {
+ display: 'flex',
+ paddingHorizontal: 20,
+ paddingVertical: 12,
+ alignItems: 'center',
+ gap: 20,
+ height: 60,
+ justifyContent: 'space-between',
+ flexDirection: 'row',
+ borderBottomWidth: 1,
+ borderColor: $config.CARD_LAYER_3_COLOR,
+ },
+ title: {
+ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high,
+ fontSize: ThemeConfig.FontSize.xLarge,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ lineHeight: 32,
+ fontWeight: '600',
+ letterSpacing: -0.48,
+ },
+ content: {
+ padding: 20,
+ gap: 20,
+ display: 'flex',
+ },
+ noPadding: {
+ padding: 0,
+ },
+ actions: {
+ display: 'flex',
+ flexDirection: 'row',
+ height: 60,
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ gap: 16,
+ alignItems: 'center',
+ flexShrink: 0,
+ borderTopWidth: 1,
+ borderTopColor: $config.CARD_LAYER_3_COLOR,
+ backgroundColor: $config.CARD_LAYER_2_COLOR,
+ },
+ alignRight: {
+ justifyContent: 'flex-end',
+ },
+});
diff --git a/polling/ui/BaseMoreButton.tsx b/polling/ui/BaseMoreButton.tsx
new file mode 100644
index 0000000..39a6bed
--- /dev/null
+++ b/polling/ui/BaseMoreButton.tsx
@@ -0,0 +1,44 @@
+import {StyleSheet, View} from 'react-native';
+import React, {forwardRef} from 'react';
+import {hexadecimalTransparency, IconButton} from 'customization-api';
+
+interface MoreMenuProps {
+ setActionMenuVisible: (f: boolean) => void;
+}
+
+const BaseMoreButton = forwardRef(
+ ({setActionMenuVisible}, ref) => {
+ return (
+
+ {
+ setActionMenuVisible(true);
+ }}
+ />
+
+ );
+ },
+);
+
+export {BaseMoreButton};
+
+const style = StyleSheet.create({
+ hoverEffect: {
+ backgroundColor:
+ $config.CARD_LAYER_5_COLOR + hexadecimalTransparency['25%'],
+ borderRadius: 18,
+ },
+ iconContainerStyle: {
+ padding: 4,
+ borderRadius: 18,
+ },
+});
diff --git a/polling/ui/BaseRadioButton.tsx b/polling/ui/BaseRadioButton.tsx
new file mode 100644
index 0000000..4ecad6a
--- /dev/null
+++ b/polling/ui/BaseRadioButton.tsx
@@ -0,0 +1,124 @@
+import {
+ TouchableOpacity,
+ View,
+ StyleSheet,
+ Text,
+ StyleProp,
+ TextStyle,
+} from 'react-native';
+import React from 'react';
+import {
+ ThemeConfig,
+ $config,
+ ImageIcon,
+ hexadecimalTransparency,
+} from 'customization-api';
+
+interface Props {
+ option: {
+ label: string;
+ value: string;
+ };
+ checked: boolean;
+ onChange: (option: string) => void;
+ labelStyle?: StyleProp;
+ filledColor?: string;
+ tickColor?: string;
+ disabled?: boolean;
+ ignoreDisabledStyle?: boolean; // Type for custom style prop
+}
+export default function BaseRadioButton(props: Props) {
+ const {
+ option,
+ checked,
+ onChange,
+ disabled = false,
+ labelStyle = {},
+ filledColor = '',
+ tickColor = '',
+ ignoreDisabledStyle = false,
+ } = props;
+ return (
+ {
+ if (disabled) {
+ return;
+ }
+ onChange(option.value);
+ }}>
+
+ {checked && (
+
+
+
+ )}
+
+ {option.label}
+
+ );
+}
+
+const style = StyleSheet.create({
+ optionsContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ width: '100%',
+ padding: 12,
+ },
+ disabledContainer: {
+ opacity: 0.5,
+ },
+ radioCircle: {
+ height: 22,
+ width: 22,
+ borderRadius: 11,
+ borderWidth: 2,
+ borderColor: $config.FONT_COLOR,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ disabledCircle: {
+ borderColor: $config.FONT_COLOR + hexadecimalTransparency['50%'],
+ },
+ radioFilled: {
+ height: 22,
+ width: 22,
+ borderRadius: 12,
+ backgroundColor: $config.FONT_COLOR,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ optionText: {
+ color: $config.FONT_COLOR,
+ fontSize: ThemeConfig.FontSize.normal,
+ fontFamily: ThemeConfig.FontFamily.sansPro,
+ fontWeight: '400',
+ lineHeight: 24,
+ marginLeft: 10,
+ },
+});