diff --git a/src/components/index.ts b/src/components/index.ts
index 07d9ca9..6365340 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1,6 +1,7 @@
export * from './buttons';
export * from './channel-icon-menu';
export * from './error-renderer';
+export * from './ui/header'
export * from './layout';
export * from './loaders';
export * from './modal';
diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx
index 682491e..649ce8f 100644
--- a/src/components/layout/index.tsx
+++ b/src/components/layout/index.tsx
@@ -22,6 +22,7 @@ export const AppContainer = styled.default.div`
export const Body = styled.default.div`
flex: 1;
padding: 1rem ${Theme.spacing.xlarge};
+ padding-top: 5.5rem; /* Account for fixed header height (3.5rem) + padding */
background: ${Theme.colors.gray900};
overflow: hidden;
display: flex;
diff --git a/src/components/menu/index.tsx b/src/components/menu/index.tsx
new file mode 100644
index 0000000..672125d
--- /dev/null
+++ b/src/components/menu/index.tsx
@@ -0,0 +1,51 @@
+/**
+ * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
+ */
+import {useLocation} from 'react-router-dom';
+import {useSelector} from 'react-redux';
+import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
+
+import {RootState} from 'store';
+import urlRoute from 'routers/url-routes';
+
+import {MenuLayout, MenuItem} from './style';
+
+interface IMenu {
+ toggleShowMenu: () => void;
+}
+
+const menuTitles = Object.keys(urlRoute).filter((menuTitle: string) => menuTitle !== 'login');
+const Menu = (props: IMenu) => {
+ const {toggleShowMenu} = props;
+ const applicationId = useSelector((state: RootState) => state.authentication.applicationId);
+ const {pathname} = useLocation();
+ const currentLocation = pathname.split('/')[2];
+
+ return (
+
+ {menuTitles.map((menuItem: string): JSX.Element | null => {
+ const {path, icon, notSupported} = urlRoute[menuItem];
+
+ if (notSupported) {
+ return null;
+ }
+
+ const active = currentLocation === path;
+
+ return (
+
+ );
+ })}
+
+ );
+};
+
+export default Menu;
\ No newline at end of file
diff --git a/src/components/menu/style.ts b/src/components/menu/style.ts
new file mode 100644
index 0000000..21429b8
--- /dev/null
+++ b/src/components/menu/style.ts
@@ -0,0 +1,40 @@
+/**
+ * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
+ */
+import * as styled from 'styled-components';
+import {Link} from 'components/ui';
+import {theme, paddings} from 'components/shared/theme';
+
+const {
+ colors,
+ primaryThemeColor
+} = theme;
+
+export const MenuLayout = styled.default.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+`;
+
+export const MenuItem = styled.default(Link)`
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: 44px;
+ padding: 0 ${paddings.xlarge};
+ color: ${colors.white};
+ text-transform: capitalize;
+ text-decoration: none;
+ cursor: pointer;
+ &.active {
+ color: ${primaryThemeColor};
+ }
+ &:hover {
+ background: ${colors.headerColor};
+ }
+ & span {
+ font-size: 22px;
+ margin: 0 ${paddings.medium} 0 0;
+ }
+`;
\ No newline at end of file
diff --git a/src/components/shared/utils.ts b/src/components/shared/utils.ts
index 9205bdd..7221462 100644
--- a/src/components/shared/utils.ts
+++ b/src/components/shared/utils.ts
@@ -2,7 +2,7 @@
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import moment, {DurationInputArg1, unitOfTime} from 'moment';
-import PlatformDetectionService from 'services/platformDetection.service';
+import PlatformDetectionService from 'services/PlatformDetection.service';
export const truncateWord = (word = '', length = 30): string => {
if (word.length >= length) {
diff --git a/src/components/side-menu/index.tsx b/src/components/side-menu/index.tsx
new file mode 100644
index 0000000..903aca2
--- /dev/null
+++ b/src/components/side-menu/index.tsx
@@ -0,0 +1,56 @@
+/**
+ * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
+ */
+import {JSX} from 'react';
+import {useAppDispatch, useAppSelector, RootState} from 'store';
+import {setPreferredTimeFormat} from 'store/action/preferred-time-format';
+import {TimeFormats} from 'utility';
+
+import {documentationLinks} from 'constants/links';
+
+import Menu from 'components/menu';
+import NewTabLink from 'components/new-tab-link';
+import Toggle from 'components/toggle';
+
+import {
+ Overlay,
+ MenuView,
+ SnapToBottom,
+ FooterContainer
+} from './style';
+import config from 'config';
+
+export const SideMenu = ({showMenu}: {showMenu: () => void }): JSX.Element => {
+ const dispatch = useAppDispatch();
+ const timeFormat = useAppSelector((state: RootState) => state.preferredTimeFormat.timeFormat);
+ const handleToggleChange = () => {
+ if (timeFormat === TimeFormats.Utc) {
+ dispatch(setPreferredTimeFormat(TimeFormats.LocalTime));
+ } else {
+ dispatch(setPreferredTimeFormat(TimeFormats.Utc));
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
\ No newline at end of file
diff --git a/src/components/side-menu/style.ts b/src/components/side-menu/style.ts
new file mode 100644
index 0000000..a73cc8b
--- /dev/null
+++ b/src/components/side-menu/style.ts
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
+ */
+import * as styled from 'styled-components';
+import {theme, paddings} from 'components/shared/theme';
+import {ToggleContainer} from 'components/toggle/style';
+
+const {colors, footerHeight, typography: {fontSizeS}} = theme;
+
+export const Overlay = styled.default.div`
+ position: fixed;
+ width: 100%;
+ min-height: calc(100% - 3.5rem - ${footerHeight});
+ top: 3.5rem;
+ right: 0;
+ background: ${colors.headerColor};
+ opacity: .7;
+ z-index: 9;
+`;
+
+export const MenuView = styled.default.div`
+ display: block;
+ position: fixed;
+ left: 0;
+ top: 3.5rem;
+ height: calc(100% - 3.5rem - ${footerHeight});
+ background-color: ${colors.black};
+ width: 292px;
+ padding-top: ${paddings.small};
+ z-index: 11;
+ box-shadow: 2px 0px 1px rgba(1,1,1, .9);
+`;
+
+export const SnapToBottom = styled.default.div`
+ width: inherit;
+ position: absolute;
+ bottom: 0;
+`;
+
+export const FooterContainer = styled.default.div`
+ width: 100%;
+ position: relative;
+ color: ${colors.gray300};
+ font-size: ${fontSizeS};
+ padding: 0 ${paddings.medium};
+
+ a {
+ text-decoration: none;
+ color: ${colors.lightBlue};
+ padding: 1rem 0;
+ display: block;
+ }
+
+ ${ToggleContainer} {
+ justify-content: flex-start;
+ }
+`;
\ No newline at end of file
diff --git a/src/components/table-screen-header/style.tsx b/src/components/table-screen-header/style.tsx
index ed441a2..c0d1fb5 100644
--- a/src/components/table-screen-header/style.tsx
+++ b/src/components/table-screen-header/style.tsx
@@ -12,7 +12,8 @@ export const ScreenHeader = styled.default.div`
align-items: baseline;
flex-wrap: wrap;
width: 100%;
- margin: 0 0 .5rem;
+ margin: 0 0 0.75rem;
+ padding: 0 ${Theme.spacing.medium};
`;
export const ScreenHeaderControls = styled.default.div`
@@ -29,9 +30,12 @@ export const HeaderTitle = styled.default.div`
align-items: baseline;
h2 {
color: ${colors.white};
- margin: 0 .5rem 0 0;
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
}
p {
color: ${colors.gray200};
+ margin: 0;
}
`;
diff --git a/src/components/table/style.tsx b/src/components/table/style.tsx
index 571ee3c..dd90c4d 100644
--- a/src/components/table/style.tsx
+++ b/src/components/table/style.tsx
@@ -23,10 +23,17 @@ export const Thead = styled.default.thead`
& th {
top: -1px;
background-color: ${primaryBackground};
- padding: ${Theme.spacing.small} 0;
+ padding: ${Theme.spacing.small} ${Theme.spacing.small};
z-index: 10;
+ text-align: left;
+ font-weight: 600;
+
+ &:first-child {
+ padding-left: ${Theme.spacing.medium};
+ }
&:last-child {
+ padding-right: ${Theme.spacing.medium};
border: none;
}
}
@@ -49,21 +56,30 @@ export const Tbody = styled.default.tbody`
}
& tr {
border-color: transparent transparent ${colors.black} transparent;
- height: 3rem;
+ height: 2.5rem;
& td {
color: ${colors.white};
- padding: ${Theme.spacing.small};
+ padding: ${Theme.spacing.xxSmall};
word-wrap: break-word;
font-size: ${typography.fontSizeS};
position: relative;
- text-align: center;
+ text-align: left;
white-space: nowrap;
- overflow: visible;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &:first-child {
+ padding-left: ${Theme.spacing.medium};
+ }
+
+ &:last-child {
+ padding-right: ${Theme.spacing.medium};
+ }
& > div,
& > p {
- text-align: center;
- justify-content: center;
+ text-align: left;
+ justify-content: flex-start;
}
& a {
diff --git a/src/components/toggle/index.tsx b/src/components/toggle/index.tsx
new file mode 100644
index 0000000..91a593a
--- /dev/null
+++ b/src/components/toggle/index.tsx
@@ -0,0 +1,39 @@
+/**
+ * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
+ */
+import {
+ ToggleContainer,
+ ToggleHiddenInput,
+ ToggleSwitchContainer,
+ ToggleSwitch,
+ ToggleTitleOn,
+ ToggleTitleOff
+} from './style';
+
+interface IToggle {
+ handleChange: () => void;
+ onTitle?: string;
+ offTitle?: string;
+ position?: 'on' | 'off';
+}
+const Toggle = (props: IToggle): JSX.Element => {
+ const {
+ onTitle,
+ offTitle,
+ handleChange,
+ position
+ } = props;
+
+ return (
+
+
+ {offTitle && {offTitle}}
+
+
+
+ {onTitle && {onTitle}}
+
+ );
+};
+
+export default Toggle;
\ No newline at end of file
diff --git a/src/components/toggle/style.ts b/src/components/toggle/style.ts
new file mode 100644
index 0000000..4c2cb18
--- /dev/null
+++ b/src/components/toggle/style.ts
@@ -0,0 +1,69 @@
+/**
+ * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
+ */
+import * as styled from 'styled-components';
+
+import {theme} from 'components/shared/theme';
+
+const {colors, typography: {fontSizeS}} = theme;
+
+export const ToggleContainer = styled.default.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
+
+const ToggleTitle = styled.default.p`
+ color: ${colors.white};
+ font-size: ${fontSizeS};
+ transition: all .2s ease-in-out;
+`;
+
+export const ToggleTitleOn = styled.default(ToggleTitle)`
+ opacity: 1;
+`;
+export const ToggleTitleOff = styled.default(ToggleTitle)`
+ opacity: 0.5;
+`;
+
+export const ToggleSwitchContainer = styled.default.label`
+ margin: 0 1rem;
+ width: 3rem;
+ height: 1.5rem;
+ background-color: transparent;
+ border: 2px solid ${colors.gray700};
+ border-radius: 40px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+ cursor: pointer;
+`;
+
+export const ToggleSwitch = styled.default.div`
+ position: absolute;
+ width: 1rem;
+ height: 1rem;
+ background-color: ${colors.lightBlue};
+ border-radius: 40px;
+ left: .2rem;
+ transition: left 200ms linear;
+`;
+
+export const ToggleHiddenInput = styled.default.input`
+ display: none;
+
+ &:checked {
+ ~${ToggleTitleOff} {
+ opacity: 0.5;
+ }
+ ~${ToggleTitleOn} {
+ opacity: 1;
+ }
+ ~${ToggleSwitchContainer} {
+ ${ToggleSwitch} {
+ left: 1.6rem;
+ }
+ }
+ }
+`;
\ No newline at end of file
diff --git a/src/components/ui/header/index.tsx b/src/components/ui/header/index.tsx
new file mode 100644
index 0000000..e8093b5
--- /dev/null
+++ b/src/components/ui/header/index.tsx
@@ -0,0 +1,140 @@
+/**
+ * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
+ */
+import {
+ useState,
+ useEffect,
+ useRef,
+ JSX
+} from 'react';
+import {useNavigate, useLocation} from 'react-router-dom';
+import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
+import {faSignOutAlt} from '@fortawesome/free-solid-svg-icons';
+
+import UserStoreService from 'services/user-store';
+import {RootState, useAppDispatch, useAppSelector} from 'store';
+import {selectIsAuthenticated, signoutThunk} from 'store/slices/Authentication.slice';
+import text from './text';
+
+import {SideMenu} from 'components/side-menu';
+import urlRoutes from 'routers/url-routes';
+
+import {
+ TopNavigation,
+ User,
+ ApplicationId,
+ UserInitials,
+ MenuIcon,
+ NavigationLeftSide
+} from './style';
+
+import logo from 'assets/images/phenix-logo-101x41.png';
+import menuIcon from 'assets/images/icon/menu.svg';
+
+const Header = (): JSX.Element => {
+ const ref = useRef(null);
+ const dispatch = useAppDispatch();
+ const {search, pathname}: {search: string; pathname: string} = useLocation();
+ const applicationId = useAppSelector((state: RootState) => state.authentication.applicationId);
+ const [viewUserDetails, setViewUserDetails] = useState(false);
+ const [showMenu, setShowMenu] = useState(false);
+ const isLoggedIn = useAppSelector(selectIsAuthenticated);
+ const navigate = useNavigate();
+ const {phenixText} = text;
+ let userInitials = '';
+
+ if (applicationId) {
+ userInitials = applicationId.substring(0, 2);
+ }
+
+ const handleClickOutside = (event: MouseEvent) => {
+ if (ref.current && !ref.current.contains(event.target as Node)) {
+ setViewUserDetails(false);
+ }
+ };
+
+ const checkAndRedirect = async(): Promise => {
+ const applicationId = await UserStoreService.get('applicationId') as string;
+
+ if (!applicationId) {
+ return navigate(`/login/${search}`);
+ }
+
+ const lastVisitedRoutes = await UserStoreService.get>('lastVisitedRouteByApplicationId') as Record;
+ const lastVisitedRoute = lastVisitedRoutes[applicationId];
+ const channelsPath = `/${urlRoutes.channels.path}/`;
+
+ if (!pathname.includes(applicationId)) {
+ if (lastVisitedRoute) {
+ return navigate(`${lastVisitedRoute}${search}`);
+ }
+
+ return navigate(`${applicationId}${channelsPath}${search}`);
+ }
+ };
+
+ // const setLastVisitedRoute = async(): Promise => {
+ // const applicationId = await userStore.get('applicationId');
+ // const lastVisitedRoutes = await userStore.get('lastVisitedRouteByApplicationId') || {};
+
+ // await userStore.set('lastVisitedRouteByApplicationId', {
+ // ...lastVisitedRoutes,
+ // [applicationId]: pathname
+ // });
+ // };
+
+ useEffect(() => {
+ checkAndRedirect();
+ }, [isLoggedIn]);
+
+ // useEffect(() => {
+ // setLastVisitedRoute();
+ // }, [pathname]);
+
+ useEffect(() => {
+ document.addEventListener('click', handleClickOutside, true);
+
+ return () => {
+ document.removeEventListener('click', handleClickOutside, true);
+ };
+ });
+
+ const handleLogout = async(): Promise => {
+ dispatch(signoutThunk());
+ };
+
+ const handleCaretClick = () => setViewUserDetails(!viewUserDetails);
+
+ console.log('isLoggedIn', isLoggedIn);
+
+ return (
+
+
+ {(isLoggedIn && applicationId) &&
+ setShowMenu(!showMenu)}
+ src={menuIcon}
+ alt="menuIcon"
+ />
+ }
+
+
+ {(isLoggedIn && applicationId) &&
+ <>
+
+ {userInitials}
+ {applicationId}
+
+
+
+
+ {showMenu &&
+ setShowMenu(!showMenu)} />
+ }
+ >
+ }
+
+ );
+};
+
+export default Header;
\ No newline at end of file
diff --git a/src/components/ui/header/style.tsx b/src/components/ui/header/style.tsx
new file mode 100644
index 0000000..c525a04
--- /dev/null
+++ b/src/components/ui/header/style.tsx
@@ -0,0 +1,76 @@
+/**
+ * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
+ */
+import * as styled from 'styled-components';
+import {theme, paddings} from 'components/shared/theme';
+
+const {colors, mediaPhone} = theme;
+
+export const TopNavigation = styled.default.div<{isLoggedIn: boolean}>`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ box-sizing: border-box;
+ padding: ${paddings.medium};
+ height: 3.5rem;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 1000;
+ ${({isLoggedIn}) => isLoggedIn && styled.css`
+ background-color: ${colors.headerColor};
+ padding: ${paddings.small} ${paddings.xlarge};
+ `}
+`;
+
+export const NavigationLeftSide = styled.default.div`
+ display: flex;
+ align-items: center;
+`;
+
+export const MenuIcon = styled.default.img`
+ margin-right: ${paddings.medium};
+ width: 1.5rem;
+`;
+
+export const User = styled.default.div`
+ display: flex;
+ align-items: center;
+ margin-left: auto;
+ color: ${colors.white};
+ gap: ${paddings.small};
+
+ & div[role="button"] {
+ cursor: pointer;
+ padding: ${paddings.xsmall};
+ border-radius: 4px;
+ transition: background-color 0.2s;
+
+ &:hover {
+ background-color: rgba(255, 255, 255, 0.1);
+ }
+ }
+`;
+
+export const ApplicationId = styled.default.p`
+ margin: 0;
+ padding: 0 ${paddings.small};
+`;
+
+export const UserInitials = styled.default.div`
+ color: ${colors.white};
+ background-color: ${colors.lightBlue};
+ display: flex;
+ font-weight: bold;
+ justify-content: center;
+ align-items: center;
+ border-radius: 50%;
+ width: 2.5rem;
+ height: 2.5rem;
+ text-transform: uppercase;
+ ${mediaPhone} {
+ display: none;
+ }
+`;
\ No newline at end of file
diff --git a/src/components/ui/header/text.ts b/src/components/ui/header/text.ts
new file mode 100644
index 0000000..18ce8dc
--- /dev/null
+++ b/src/components/ui/header/text.ts
@@ -0,0 +1,7 @@
+/**
+ * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
+ */
+export default {
+ applicationIdText: 'Application Id',
+ phenixText: 'Phenix'
+};
\ No newline at end of file
diff --git a/src/routers/index.tsx b/src/routers/index.tsx
index 338cc62..b338b46 100644
--- a/src/routers/index.tsx
+++ b/src/routers/index.tsx
@@ -1,18 +1,24 @@
import {BrowserRouter, Route, Routes, Navigate} from 'react-router-dom';
import {ProtectedRoute} from 'components';
-import {LoginForm, ChannelList, ChannelDetail} from 'views';
+import {LoginForm} from 'views';
+import Header from 'components/ui/header';
+import {lazy, Suspense} from 'react';
+
+const ChannelList = lazy(() => import('views/ChannelList').then(module => ({default: module.ChannelList})));
+const ChannelDetail = lazy(() => import('views/ChannelDetail').then(module => ({default: module.ChannelDetail})));
export default function Router() {
return (
+
{/* Public routes */}
- } />
+ } />
{/* Protected routes */}
} />
- } />} />
- } />} />
+ } />} />
+ } />} />
{/* Fallback route */}
} />
diff --git a/src/routers/url-routes.ts b/src/routers/url-routes.ts
new file mode 100644
index 0000000..76e6660
--- /dev/null
+++ b/src/routers/url-routes.ts
@@ -0,0 +1,29 @@
+/**
+ * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
+ */
+import {faStream, faChartBar, faLayerGroup, faCheckSquare, faTh} from '@fortawesome/free-solid-svg-icons';
+
+export default {
+ login: {path: '/login'},
+ channels: {
+ path: 'channels',
+ icon: faStream
+ },
+ rooms: {
+ path: 'rooms',
+ icon: faLayerGroup
+ },
+ analytics: {
+ path: 'analytics',
+ icon: faChartBar
+ },
+ qos: {
+ path: 'qos',
+ icon: faCheckSquare,
+ notSupported: true
+ },
+ dashboard: {
+ path: 'dashboard',
+ icon: faTh
+ }
+};
\ No newline at end of file
diff --git a/src/services/Authentication.service.ts b/src/services/Authentication.service.ts
index 79bcfcd..3fcc87b 100644
--- a/src/services/Authentication.service.ts
+++ b/src/services/Authentication.service.ts
@@ -31,6 +31,10 @@ class AuthenticationService {
return this._token;
}
+ public hasLoginToken(): boolean {
+ return this._token !== null;
+ }
+
async authenticate(applicationId: string, secret: string): Promise {
const authenticate = {
// @ts-expect-error TODO(AZ): phenix-web-proto does not have Typescript types defined definition
@@ -39,8 +43,8 @@ class AuthenticationService {
deviceId: '',
platform: PlatformDetectionService.platform,
platformVersion: PlatformDetectionService.platformVersion,
- browser: PlatformDetectionService.browser,
- browserVersion: PlatformDetectionService.version,
+ browser: PlatformDetectionService.browserName,
+ browserVersion: PlatformDetectionService.browserVersion,
applicationId,
authenticationToken: secret,
sessionId: this.sessionId
diff --git a/src/services/files/countries_mapping.json b/src/services/files/countries_mapping.json
new file mode 100644
index 0000000..3b9c723
--- /dev/null
+++ b/src/services/files/countries_mapping.json
@@ -0,0 +1,177 @@
+{"AF": "Afghanistan",
+ "AO": "Angola",
+ "AL": "Albania",
+ "AE": "United Arab Emirates",
+ "AR": "Argentina",
+ "AM": "Armenia",
+ "AQ": "Antarctica",
+ "TF": "French Southern and Antarctic Lands",
+ "AU": "Australia",
+ "AT": "Austria",
+ "AZ": "Azerbaijan",
+ "BI": "Burundi",
+ "BE": "Belgium",
+ "BJ": "Benin",
+ "BF": "Burkina Faso",
+ "BD": "Bangladesh",
+ "BG": "Bulgaria",
+ "BS": "The Bahamas",
+ "BA": "Bosnia and Herzegovina",
+ "BY": "Belarus",
+ "BZ": "Belize",
+ "BO": "Bolivia",
+ "BR": "Brazil",
+ "BN": "Brunei",
+ "BT": "Bhutan",
+ "BW": "Botswana",
+ "CF": "Central African Republic",
+ "CA": "Canada",
+ "CH": "Switzerland",
+ "CL": "Chile",
+ "CN": "China",
+ "CI": "Ivory Coast",
+ "CM": "Cameroon",
+ "CD": "Democratic Republic of the Congo",
+ "CG": "Republic of the Congo",
+ "CO": "Colombia",
+ "CR": "Costa Rica",
+ "CU": "Cuba",
+ "-99": "Northern Cyprus",
+ "CY": "Cyprus",
+ "CZ": "Czech Republic",
+ "DE": "Germany",
+ "DJ": "Djibouti",
+ "DK": "Denmark",
+ "DO": "Dominican Republic",
+ "DZ": "Algeria",
+ "EC": "Ecuador",
+ "EG": "Egypt",
+ "ER": "Eritrea",
+ "ES": "Spain",
+ "EE": "Estonia",
+ "ET": "Ethiopia",
+ "FI": "Finland",
+ "FJ": "Fiji",
+ "FK": "Falkland Islands",
+ "FR": "France",
+ "GA": "Gabon",
+ "GB": "England",
+ "GE": "Georgia",
+ "GH": "Ghana",
+ "GN": "Guinea",
+ "GM": "Gambia",
+ "GW": "Guinea Bissau",
+ "GQ": "Equatorial Guinea",
+ "GR": "Greece",
+ "GL": "Greenland",
+ "GT": "Guatemala",
+ "GY": "Guyana",
+ "HN": "Honduras",
+ "HR": "Croatia",
+ "HT": "Haiti",
+ "HU": "Hungary",
+ "ID": "Indonesia",
+ "IN": "India",
+ "IE": "Ireland",
+ "IR": "Iran",
+ "IQ": "Iraq",
+ "IS": "Iceland",
+ "IL": "Israel",
+ "IT": "Italy",
+ "JM": "Jamaica",
+ "JO": "Jordan",
+ "JP": "Japan",
+ "KZ": "Kazakhstan",
+ "KE": "Kenya",
+ "KG": "Kyrgyzstan",
+ "KH": "Cambodia",
+ "KR": "South Korea",
+ "OSA": "Kosovo",
+ "KW": "Kuwait",
+ "LA": "Laos",
+ "LB": "Lebanon",
+ "LR": "Liberia",
+ "LY": "Libya",
+ "LK": "Sri Lanka",
+ "LS": "Lesotho",
+ "LT": "Lithuania",
+ "LU": "Luxembourg",
+ "LV": "Latvia",
+ "MA": "Morocco",
+ "MD": "Moldova",
+ "MG": "Madagascar",
+ "MX": "Mexico",
+ "MK": "Macedonia",
+ "ML": "Mali",
+ "MM": "Myanmar",
+ "MNE": "Montenegro",
+ "MN": "Mongolia",
+ "MZ": "Mozambique",
+ "MR": "Mauritania",
+ "MW": "Malawi",
+ "MY": "Malaysia",
+ "NA": "Namibia",
+ "NC": "New Caledonia",
+ "NE": "Niger",
+ "NG": "Nigeria",
+ "NI": "Nicaragua",
+ "NL": "Netherlands",
+ "NO": "Norway",
+ "NP": "Nepal",
+ "NZ": "New Zealand",
+ "OM": "Oman",
+ "PK": "Pakistan",
+ "PA": "Panama",
+ "PE": "Peru",
+ "PH": "Philippines",
+ "PG": "Papua New Guinea",
+ "PL": "Poland",
+ "PR": "Puerto Rico",
+ "KP": "North Korea",
+ "PT": "Portugal",
+ "PY": "Paraguay",
+ "QA": "Qatar",
+ "RO": "Romania",
+ "RU": "Russia",
+ "RW": "Rwanda",
+ "EH": "Western Sahara",
+ "SA": "Saudi Arabia",
+ "SD": "Sudan",
+ "SDS": "South Sudan",
+ "SN": "Senegal",
+ "SB": "Solomon Islands",
+ "SL": "Sierra Leone",
+ "SV": "El Salvador",
+ "ABV": "Somaliland",
+ "SO": "Somalia",
+ "SRB": "Republic of Serbia",
+ "SR": "Suriname",
+ "SK": "Slovakia",
+ "SI": "Slovenia",
+ "SE": "Sweden",
+ "SZ": "Swaziland",
+ "SY": "Syria",
+ "TD": "Chad",
+ "TG": "Togo",
+ "TH": "Thailand",
+ "TJ": "Tajikistan",
+ "TM": "Turkmenistan",
+ "TL": "East Timor",
+ "TT": "Trinidad and Tobago",
+ "TN": "Tunisia",
+ "TR": "Turkey",
+ "TW": "Taiwan",
+ "TZ": "United Republic of Tanzania",
+ "UG": "Uganda",
+ "UA": "Ukraine",
+ "UY": "Uruguay",
+ "US": "USA",
+ "UZ": "Uzbekistan",
+ "VE": "Venezuela",
+ "VN": "Vietnam",
+ "VU": "Vanuatu",
+ "PS": "West Bank",
+ "YE": "Yemen",
+ "ZA": "South Africa",
+ "ZM": "Zambia",
+ "ZW": "Zimbabwe"}
\ No newline at end of file
diff --git a/src/services/user-store/IUserDataStore.ts b/src/services/user-store/IUserDataStore.ts
index 0ce7018..a0fb2de 100644
--- a/src/services/user-store/IUserDataStore.ts
+++ b/src/services/user-store/IUserDataStore.ts
@@ -1,4 +1,4 @@
export default interface IUserDataStore {
- get(key: string, defaultValue: string): Promise;
+ get(key: string, defaultValue?: T): Promise;
set(key: string, value: string): Promise;
}
diff --git a/src/store/action/preferred-time-format.ts b/src/store/action/preferred-time-format.ts
new file mode 100644
index 0000000..9a4fe35
--- /dev/null
+++ b/src/store/action/preferred-time-format.ts
@@ -0,0 +1,132 @@
+/**
+ * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
+ */
+import {Dispatch} from 'redux';
+import userStore from 'services/user-store';
+import {TimeFormats} from 'utility';
+import {transformToPortalError} from 'utility/error-handler';
+
+/**
+ * ACTION TYPES
+ */
+
+export const GET_PREFERRED_TIME_FORMAT = 'preferred-time-format::GET_PREFERRED_TIME_FORMAT' as const;
+export const GET_PREFERRED_TIME_FORMAT_DONE = 'preferred-time-format::GET_PREFERRED_TIME_FORMAT_DONE' as const;
+export const GET_PREFERRED_TIME_FORMAT_ERROR = 'preferred-time-format::GET_PREFERRED_TIME_FORMAT_ERROR' as const;
+export const SET_PREFERRED_TIME_FORMAT = 'preferred-time-format::SET_PREFERRED_TIME_FORMAT' as const;
+export const SET_PREFERRED_TIME_FORMAT_DONE = 'preferred-time-format::SET_PREFERRED_TIME_FORMAT_DONE' as const;
+export const SET_PREFERRED_TIME_FORMAT_ERROR = 'preferred-time-format::SET_PREFERRED_TIME_FORMAT_ERROR' as const;
+
+/**
+ * INTERFACES
+ */
+
+interface IRequestGetPreferredTimeFormat {
+ type: typeof GET_PREFERRED_TIME_FORMAT;
+}
+interface IReceiveGetPreferredTimeFormat {
+ type: typeof GET_PREFERRED_TIME_FORMAT_DONE;
+ payload: {data: TimeFormats};
+}
+interface IFailedGetPreferredTimeFormat {
+ type: typeof GET_PREFERRED_TIME_FORMAT_ERROR;
+ error: null | string;
+}
+interface IRequestSetPreferredTimeFormat {
+ type: typeof SET_PREFERRED_TIME_FORMAT;
+}
+interface IReceiveSetPreferredTimeFormat {
+ type: typeof SET_PREFERRED_TIME_FORMAT_DONE;
+ payload: {data: TimeFormats};
+}
+interface IFailedSetPreferredTimeFormat {
+ type: typeof SET_PREFERRED_TIME_FORMAT_ERROR;
+ error: null | string;
+}
+
+export type GetPreferredTimeFormatActionType = IRequestGetPreferredTimeFormat | IReceiveGetPreferredTimeFormat | IFailedGetPreferredTimeFormat;
+export type SetPreferredTimeFormatActionType = IRequestSetPreferredTimeFormat | IReceiveSetPreferredTimeFormat | IFailedSetPreferredTimeFormat;
+
+interface IGetPreferredTimeFormatActions {
+ request: () => GetPreferredTimeFormatActionType;
+ receive: (payload) => GetPreferredTimeFormatActionType;
+ failed: (error: null | string) => GetPreferredTimeFormatActionType;
+}
+interface ISetPreferredTimeFormatActions {
+ request: () => SetPreferredTimeFormatActionType;
+ receive: (payload) => SetPreferredTimeFormatActionType;
+ failed: (error: null | string) => SetPreferredTimeFormatActionType;
+}
+
+/**
+ * ACTIONS
+ */
+
+const getPreferredTimeFormatActions: IGetPreferredTimeFormatActions = {
+ request: () => ({type: GET_PREFERRED_TIME_FORMAT}),
+ receive: payload => ({
+ type: GET_PREFERRED_TIME_FORMAT_DONE,
+ payload
+ }),
+ failed: error => ({
+ type: GET_PREFERRED_TIME_FORMAT_ERROR,
+ error
+ })
+};
+
+export const getPreferredTimeFormat = () => async(dispatch: Dispatch): Promise => {
+ const {
+ request,
+ receive,
+ failed
+ } = getPreferredTimeFormatActions;
+
+ dispatch(request());
+
+ try {
+ let preferredTimeFormat = await userStore.get('timeFormat');
+
+ if (!preferredTimeFormat) {
+ await userStore.set('timeFormat', TimeFormats.Utc);
+ preferredTimeFormat = await userStore.get('timeFormat');
+ }
+
+ dispatch(receive({data: preferredTimeFormat}));
+ } catch (e) {
+ const {message} = transformToPortalError(e);
+
+ dispatch(failed(message || 'An error occurred while getting the preferred time format'));
+ }
+};
+
+const setPreferredTimeFormatActions: ISetPreferredTimeFormatActions = {
+ request: () => ({type: SET_PREFERRED_TIME_FORMAT}),
+ receive: payload => ({
+ type: SET_PREFERRED_TIME_FORMAT_DONE,
+ payload
+ }),
+ failed: error => ({
+ type: SET_PREFERRED_TIME_FORMAT_ERROR,
+ error
+ })
+};
+
+export const setPreferredTimeFormat = (format: TimeFormats) => async(dispatch: Dispatch): Promise => {
+ const {
+ request,
+ receive,
+ failed
+ } = setPreferredTimeFormatActions;
+
+ dispatch(request());
+
+ try {
+ await userStore.set('timeFormat', format);
+
+ dispatch(receive({data: format}));
+ } catch (e) {
+ const {message} = transformToPortalError(e);
+
+ dispatch(failed(message || 'An error occurred while setting the preferred time format'));
+ }
+};
\ No newline at end of file
diff --git a/src/store/middlewares/authenticationMiddleware.ts b/src/store/middlewares/authenticationMiddleware.ts
index 7f83955..48075db 100644
--- a/src/store/middlewares/authenticationMiddleware.ts
+++ b/src/store/middlewares/authenticationMiddleware.ts
@@ -1,5 +1,5 @@
import {
- authenticateCredentialsThunk,
+ authenticateCredentials,
selectIsAuthenticated,
selectIsLoading,
selectApplicationId,
@@ -56,7 +56,7 @@ export const authenticateRequestMiddleware: Middleware = store => next => async
try {
console.log('[authenticateRequest] Attempting auto-authentication');
// Use the Redux thunk to properly update the state
- const authResult = await store.dispatch(authenticateCredentialsThunk({applicationId, secret}) as any);
+ const authResult = await store.dispatch(authenticateCredentials({applicationId, secret}) as any);
if (authResult.type.endsWith('/rejected') || authResult.payload === 'Authentication failed') {
console.log('[authenticateRequest] Authentication failed');
diff --git a/src/store/slices/Authentication.slice.ts b/src/store/slices/Authentication.slice.ts
index 8c6eff5..cba2c77 100644
--- a/src/store/slices/Authentication.slice.ts
+++ b/src/store/slices/Authentication.slice.ts
@@ -60,7 +60,7 @@ export const selectHasRole = createSelector([selectAuthentication, (_, role: str
export const selectIsOnline = createSelector([selectAuthentication], authentication => authentication.status === 'Online');
-const authenticateCredentialsThunk = createAsyncThunk(
+const authenticateCredentials = createAsyncThunk(
'authentication/authenticate',
async (credentials, {rejectWithValue}) => {
try {
@@ -76,7 +76,7 @@ const authenticateCredentialsThunk = createAsyncThunk {
+export const signoutThunk = createAsyncThunk('authentication/signout', async (_, {rejectWithValue}) => {
try {
return await AuthenticationService.signout();
} catch (error) {
@@ -95,8 +95,15 @@ const authenticationSlice = createSlice({
state.isLoading = action.payload;
},
setCredentials: (state, action: PayloadAction<{applicationId: string; secret: string}>) => {
- state.applicationId = action.payload.applicationId;
- state.secret = action.payload.secret;
+ const {applicationId, secret} = action.payload;
+
+ if (applicationId) {
+ state.applicationId = applicationId;
+ }
+
+ if (secret) {
+ state.secret = secret;
+ }
},
clearState: state => {
state.applicationId = null;
@@ -125,15 +132,14 @@ const authenticationSlice = createSlice({
},
extraReducers: builder => {
builder
- .addCase(authenticateCredentialsThunk.pending, state => {
+ .addCase(authenticateCredentials.pending, state => {
state.isLoading = true;
state.error = null;
})
- .addCase(authenticateCredentialsThunk.fulfilled, (state, action) => {
+ .addCase(authenticateCredentials.fulfilled, (state, action) => {
const authenticationResponse = action.payload;
if (authenticationResponse.status === 'ok') {
- state.applicationId = authenticationResponse.applicationId ?? null;
state.sessionId = authenticationResponse.sessionId ?? null;
state.isAuthenticated = true;
state.roles = authenticationResponse.roles ?? [];
@@ -156,7 +162,7 @@ const authenticationSlice = createSlice({
state.isLoading = false;
}
})
- .addCase(authenticateCredentialsThunk.rejected, (state, action) => {
+ .addCase(authenticateCredentials.rejected, (state, action) => {
state.applicationId = null;
state.sessionId = null;
state.isAuthenticated = false;
@@ -190,5 +196,5 @@ const authenticationSlice = createSlice({
});
export const {setUnauthorized, setIsLoading, setCredentials, clearState, setSessionId, setError} = authenticationSlice.actions;
-export {authenticateCredentialsThunk};
+export {authenticateCredentials};
export default authenticationSlice.reducer;
diff --git a/src/store/slices/PreferredTimeFormat.slice.ts b/src/store/slices/PreferredTimeFormat.slice.ts
new file mode 100644
index 0000000..7515282
--- /dev/null
+++ b/src/store/slices/PreferredTimeFormat.slice.ts
@@ -0,0 +1,27 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+import { TimeFormats } from "utility";
+
+export interface IPreferredTimeFormatState {
+ isLoading: boolean;
+ error: null | string;
+ timeFormat: TimeFormats;
+}
+
+export const initialPreferredTimeFormatState: IPreferredTimeFormatState = {
+ isLoading: false,
+ error: null,
+ timeFormat: TimeFormats.Utc
+}
+
+export const preferredTimeFormatSlice = createSlice({
+ name: 'preferredTimeFormat',
+ initialState: initialPreferredTimeFormatState,
+ reducers: {
+ setPreferredTimeFormat: (state: IPreferredTimeFormatState, action: PayloadAction) => {
+ state.timeFormat = action.payload;
+ }
+ }
+})
+
+export const { setPreferredTimeFormat } = preferredTimeFormatSlice.actions;
+export default preferredTimeFormatSlice.reducer;
\ No newline at end of file
diff --git a/src/store/store.ts b/src/store/store.ts
index ebfe505..1b6aeb5 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -3,6 +3,7 @@ import AuthenticationReducer from './slices/Authentication.slice';
import ChannelsReducer from './slices/Channels.slice';
import ChannelsPublishingReducer from './action/channels-publishing';
import ScreensReducer from './action/screens';
+import PreferredTimeFormatReducer from './slices/PreferredTimeFormat.slice';
import {authenticateRequestMiddleware, loggerMiddleware, vanillaPromiseMiddleware} from './middlewares';
const store = configureStore({
@@ -10,7 +11,8 @@ const store = configureStore({
authentication: AuthenticationReducer,
channels: ChannelsReducer,
channelsPublishing: ChannelsPublishingReducer,
- screens: ScreensReducer
+ screens: ScreensReducer,
+ preferredTimeFormat: PreferredTimeFormatReducer
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
diff --git a/src/utility/custom-hooks.ts b/src/utility/custom-hooks.ts
index 299ef52..92c8f55 100644
--- a/src/utility/custom-hooks.ts
+++ b/src/utility/custom-hooks.ts
@@ -29,7 +29,7 @@ export const useComponentVisible = (
setIsComponentVisible: Dispatch>;
} => {
const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible);
- const ref = useRef(null);
+ const ref = useRef(null);
const handleClickOutside = () => {
setIsComponentVisible(false);
};
@@ -98,7 +98,7 @@ export const useLoginStatus = (applicationId: string): [boolean] => {
const [isLoggedIn, setLoginStatus] = useState(false);
useEffect(() => {
- AuthService.hasLoginToken().then((response: boolean) => setLoginStatus(response));
+ setLoginStatus(AuthService.hasLoginToken());
}, [applicationId]);
return [isLoggedIn];
diff --git a/src/utility/date.ts b/src/utility/date.ts
index 4440bb6..c87f793 100644
--- a/src/utility/date.ts
+++ b/src/utility/date.ts
@@ -26,7 +26,9 @@ export const fetchPlatformTimeFromServer = async (): Promise => {
const localTimeBeforeCall = moment.utc().utc().valueOf();
const headers = new Headers();
- headers.set('Authorization', `Basic ${await userStore.get(AuthService.token, '')}`);
+ if (AuthService.token) {
+ headers.set('Authorization', `Basic ${await userStore.get(AuthService.token, '')}`);
+ }
const response = await fetch('https://phenixrts.com/video/dash/time.txt', {headers});
const localTimeAfterCall = moment.utc().utc().valueOf();
@@ -46,8 +48,8 @@ export const getAdjustedTime = (): Moment => {
export const getTimezoneAbbreviation = (date: Date): string => {
const timezone = date
.toString()
- .match(/\(.+\)/g)[0]
- .replace('(', '')
+ .match(/\(.+\)/g)?.[0] ?? ''
+ .replace('(', '') ?? ''
.replace(')', '');
let abbreviation = '';
diff --git a/src/views/LoginForm/LoginForm.tsx b/src/views/LoginForm/LoginForm.tsx
index abaf499..1345280 100644
--- a/src/views/LoginForm/LoginForm.tsx
+++ b/src/views/LoginForm/LoginForm.tsx
@@ -4,7 +4,7 @@
import {FC, useState, useEffect} from 'react';
import text from './text';
import {useAppDispatch, useAppSelector} from 'store/index';
-import {authenticateCredentialsThunk, selectIsLoading, selectError, setError, selectIsAuthenticated, setCredentials} from 'store/slices/Authentication.slice';
+import {authenticateCredentials, selectIsLoading, selectError, setError, selectIsAuthenticated, setCredentials} from 'store/slices/Authentication.slice';
import {
LoginFormBackground,
LogoContainer,
@@ -55,7 +55,7 @@ export const LoginForm: FC = () => {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
dispatch(setCredentials({applicationId, secret}));
- dispatch(authenticateCredentialsThunk({applicationId, secret}));
+ dispatch(authenticateCredentials({applicationId, secret}));
};
const handleInputChange = (setter: (value: string) => void) => (e: React.ChangeEvent) => {