summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOliver <oliver@mohlin.dev>2025-01-07 11:40:51 +0100
committerMarkus Pettersson <markus.pettersson@mullvad.net>2025-01-13 15:02:51 +0100
commit92852eb1cffab60f6979877c12c8629a9694153d (patch)
tree11a5ab89e45b7df325c784440c16d7e57b59e57b
parent33caf981637a2391eb786293cf52c2c11d8dc548 (diff)
downloadmullvadvpn-92852eb1cffab60f6979877c12c8629a9694153d.tar.xz
mullvadvpn-92852eb1cffab60f6979877c12c8629a9694153d.zip
Add MainHeader component
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/main-header/MainHeader.tsx80
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/MainHeaderAccountButton.tsx33
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/MainHeaderDeviceInfo.tsx54
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/MainHeaderSettingsButton.tsx27
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/index.ts3
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/main-header/index.ts1
6 files changed, 198 insertions, 0 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/main-header/MainHeader.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/main-header/MainHeader.tsx
new file mode 100644
index 0000000000..b3d2d7f676
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/main-header/MainHeader.tsx
@@ -0,0 +1,80 @@
+import { TunnelState } from '../../../shared/daemon-rpc-types';
+import { Flex, Header, HeaderProps, Logo, LogoProps } from '../../lib/components';
+import { Spacings } from '../../lib/foundations';
+import { useSelector } from '../../redux/store';
+import { FocusFallback } from '../Focus';
+import {
+ MainHeaderBarAccountButton,
+ MainHeaderDeviceInfo,
+ MainHeaderSettingsButton,
+} from './components';
+
+export interface MainHeaderProps extends Omit<HeaderProps, 'variant' | 'size'> {
+ variant?: HeaderProps['variant'] | 'basedOnConnectionStatus';
+ size?: HeaderProps['size'] | 'basedOnLoginStatus';
+ logoVariant?: LogoProps['variant'] | 'none';
+ children?: React.ReactNode;
+}
+
+const MainHeader = ({
+ logoVariant = 'both',
+ variant: variantProp,
+ size: sizeProp,
+ children,
+ ...props
+}: MainHeaderProps) => {
+ const connectionStatus = useSelector((state) => state.connection.status);
+
+ const variant =
+ variantProp === 'basedOnConnectionStatus'
+ ? getVariantByTunnelState(connectionStatus)
+ : variantProp;
+
+ const loggedIn = useSelector((state) => state.account.status.type === 'ok');
+ const size = sizeProp === 'basedOnLoginStatus' ? (loggedIn ? '2' : '1') : sizeProp;
+
+ return (
+ <Header variant={variant} size={size} {...props}>
+ <Header.MainRow>
+ <FocusFallback>
+ {logoVariant !== 'none' ? <Logo variant={logoVariant} /> : <div />}
+ </FocusFallback>
+ <Flex $gap={Spacings.spacing5} $alignItems="center">
+ {children}
+ </Flex>
+ </Header.MainRow>
+ {size == '2' && (
+ <Header.SubRow>
+ <MainHeaderDeviceInfo />
+ </Header.SubRow>
+ )}
+ </Header>
+ );
+};
+
+const MainHeaderNamespace = Object.assign(MainHeader, {
+ AccountButton: MainHeaderBarAccountButton,
+ SettingsButton: MainHeaderSettingsButton,
+});
+
+export { MainHeaderNamespace as MainHeader };
+
+const getVariantByTunnelState = (tunnelState: TunnelState): HeaderProps['variant'] => {
+ switch (tunnelState.state) {
+ case 'disconnected':
+ return 'error';
+ case 'connecting':
+ case 'connected':
+ return 'success';
+ case 'error':
+ return !tunnelState.details.blockingError ? 'success' : 'error';
+ case 'disconnecting':
+ switch (tunnelState.details) {
+ case 'block':
+ case 'reconnect':
+ return 'success';
+ case 'nothing':
+ return 'error';
+ }
+ }
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/MainHeaderAccountButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/MainHeaderAccountButton.tsx
new file mode 100644
index 0000000000..b7a440a9d4
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/MainHeaderAccountButton.tsx
@@ -0,0 +1,33 @@
+import { useCallback } from 'react';
+
+import { messages } from '../../../../shared/gettext';
+import { IconButton, IconButtonProps } from '../../../lib/components';
+import { transitions, useHistory } from '../../../lib/history';
+import { RoutePath } from '../../../lib/routes';
+import { useSelector } from '../../../redux/store';
+
+export type MainHeaderBarAccountButtonProps = Omit<IconButtonProps, 'icon'>;
+
+export const MainHeaderBarAccountButton = (props: MainHeaderBarAccountButtonProps) => {
+ const history = useHistory();
+ const openAccount = useCallback(
+ () => history.push(RoutePath.account, { transition: transitions.show }),
+ [history],
+ );
+
+ const loggedIn = useSelector((state) => state.account.status.type === 'ok');
+ if (!loggedIn) {
+ return null;
+ }
+
+ return (
+ <IconButton
+ icon="icon-account"
+ variant="secondary"
+ onClick={openAccount}
+ data-testid="account-button"
+ aria-label={messages.gettext('Account settings')}
+ {...props}
+ />
+ );
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/MainHeaderDeviceInfo.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/MainHeaderDeviceInfo.tsx
new file mode 100644
index 0000000000..4bf2b6a142
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/MainHeaderDeviceInfo.tsx
@@ -0,0 +1,54 @@
+import { sprintf } from 'sprintf-js';
+import styled from 'styled-components';
+
+import { closeToExpiry, formatRemainingTime, hasExpired } from '../../../../shared/account-expiry';
+import { messages } from '../../../../shared/gettext';
+import { capitalizeEveryWord } from '../../../../shared/string-helpers';
+import { FootnoteMini } from '../../../lib/components';
+import { Colors } from '../../../lib/foundations';
+import { useSelector } from '../../../redux/store';
+
+const StyledTimeLeftLabel = styled(FootnoteMini)({
+ whiteSpace: 'nowrap',
+});
+
+const StyledDeviceLabel = styled(FootnoteMini)({
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+});
+
+export const MainHeaderDeviceInfo = () => {
+ const deviceName = useSelector((state) => state.account.deviceName);
+ const accountExpiry = useSelector((state) => state.account.expiry);
+ const isOutOfTime = accountExpiry ? hasExpired(accountExpiry) : false;
+ const formattedExpiry = isOutOfTime
+ ? sprintf(messages.ngettext('1 day', '%d days', 0), 0)
+ : accountExpiry
+ ? formatRemainingTime(accountExpiry)
+ : '';
+
+ return (
+ <>
+ <StyledDeviceLabel color={Colors.white80}>
+ {sprintf(
+ // TRANSLATORS: A label that will display the newly created device name to inform the user
+ // TRANSLATORS: about it.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(deviceName)s - The name of the current device
+ messages.pgettext('device-management', 'Device name: %(deviceName)s'),
+ {
+ deviceName: capitalizeEveryWord(deviceName ?? ''),
+ },
+ )}
+ </StyledDeviceLabel>
+ {accountExpiry && !closeToExpiry(accountExpiry) && !isOutOfTime && (
+ <StyledTimeLeftLabel color={Colors.white80}>
+ {sprintf(messages.pgettext('device-management', 'Time left: %(timeLeft)s'), {
+ timeLeft: formattedExpiry,
+ })}
+ </StyledTimeLeftLabel>
+ )}
+ </>
+ );
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/MainHeaderSettingsButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/MainHeaderSettingsButton.tsx
new file mode 100644
index 0000000000..fa2b1f1c5f
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/MainHeaderSettingsButton.tsx
@@ -0,0 +1,27 @@
+import { useCallback } from 'react';
+
+import { messages } from '../../../../shared/gettext';
+import { IconButton, IconButtonProps } from '../../../lib/components';
+import { transitions, useHistory } from '../../../lib/history';
+import { RoutePath } from '../../../lib/routes';
+
+export type MainHeaderSettingsButtonProps = Omit<IconButtonProps, 'icon'>;
+
+export function MainHeaderSettingsButton(props: MainHeaderSettingsButtonProps) {
+ const history = useHistory();
+
+ const openSettings = useCallback(() => {
+ if (!props.disabled) {
+ history.push(RoutePath.settings, { transition: transitions.show });
+ }
+ }, [history, props.disabled]);
+
+ return (
+ <IconButton
+ icon="icon-settings"
+ variant="secondary"
+ onClick={openSettings}
+ aria-label={messages.gettext('Settings')}
+ />
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/index.ts
new file mode 100644
index 0000000000..9ff843a82c
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/main-header/components/index.ts
@@ -0,0 +1,3 @@
+export * from './MainHeaderDeviceInfo';
+export * from './MainHeaderAccountButton';
+export * from './MainHeaderSettingsButton';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/main-header/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/main-header/index.ts
new file mode 100644
index 0000000000..3d8a1927af
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/main-header/index.ts
@@ -0,0 +1 @@
+export * from './MainHeader';