- {canSelectCommunityForDevelopment() && (
-
- }
- />
- )}
{renderItemsVisibleFromCommunity()}
{renderBasePubPubLinks()}
{renderUserMenuOrLogin()}
diff --git a/client/components/GlobalControls/UserMenu.tsx b/client/components/GlobalControls/UserMenu.tsx
index df897ef33f..ace9f4e16c 100644
--- a/client/components/GlobalControls/UserMenu.tsx
+++ b/client/components/GlobalControls/UserMenu.tsx
@@ -2,12 +2,20 @@ import type { MenuDisclosureProps } from 'reakit/Menu';
import type { LoginData } from 'types';
-import React from 'react';
+import React, { useCallback, useState } from 'react';
-import { Button } from '@blueprintjs/core';
+import { Button, Classes } from '@blueprintjs/core';
import { apiFetch } from 'client/utils/apiFetch';
-import { Avatar, Menu, MenuItem, MobileAware, type MobileAwareRenderProps } from 'components';
+import {
+ Avatar,
+ Menu,
+ MenuItem,
+ MenuItemDivider,
+ MobileAware,
+ type MobileAwareRenderProps,
+} from 'components';
+import { usePageContext } from 'utils/hooks';
type Props = {
loginData: LoginData;
@@ -56,15 +64,179 @@ const renderDisclosure = (loginData: LoginData, disclosureProps: MenuDisclosureP
);
};
+type UserCommunity = {
+ id: string;
+ title: string;
+ subdomain: string;
+ domain: string | null;
+ avatar: string | null;
+ headerLogo: string | null;
+ accentColorDark: string | null;
+};
+
+const getCommunityUrl = (community: UserCommunity, locationData: { isDuqDuq: boolean }) => {
+ if (locationData.isDuqDuq) {
+ return `https://${community.subdomain}.duqduq.org`;
+ }
+ return community.domain
+ ? `https://${community.domain}`
+ : `https://${community.subdomain}.pubpub.org`;
+};
+
+const getCommunityDisplayUrl = (community: UserCommunity) => {
+ return community.domain || `${community.subdomain}.pubpub.org`;
+};
+
+const CommunityAvatar = ({ community, size = 24 }: { community: UserCommunity; size?: number }) => {
+ const bgColor = community.accentColorDark || '#607D8B';
+ const initial = community.title.charAt(0).toUpperCase();
+ if (community.avatar) {
+ return (
+

+ );
+ }
+ return (
+
+ {initial}
+
+ );
+};
+
+const SkeletonRow = () => (
+
+
+
+
+);
+
+const YourCommunitiesSection = ({ communities }: { communities: UserCommunity[] | null }) => {
+ const { locationData } = usePageContext();
+
+ // Hide entirely if no communities
+ if (communities && communities.length === 0) return null;
+
+ return (
+ <>
+
+ Your Communities
+
+
+ {!communities ? (
+ <>
+
+
+
+ >
+ ) : (
+ communities.map((community) => (
+
}
+ className="community-menu-item"
+ menuStyle={{
+ display: 'flex',
+ alignItems: 'center',
+ }}
+ text={
+
+
+ {community.title}
+
+
+ {getCommunityDisplayUrl(community)}
+
+
+ }
+ />
+ ))
+ )}
+
+ >
+ );
+};
+
const UserMenu = (props: Props) => {
const { loginData } = props;
+ const [communities, setCommunities] = useState
(null);
+ const [hasFetched, setHasFetched] = useState(false);
+
+ const handleVisibleChange = useCallback(
+ (visible: boolean) => {
+ if (visible && !hasFetched) {
+ setHasFetched(true);
+ apiFetch
+ .get('/api/users/communities')
+ .then(setCommunities)
+ .catch(() => setCommunities([]));
+ }
+ },
+ [hasFetched],
+ );
+
return (
);
};
diff --git a/client/components/GlobalControls/globalControls.scss b/client/components/GlobalControls/globalControls.scss
index 8059c67076..09df8e089e 100644
--- a/client/components/GlobalControls/globalControls.scss
+++ b/client/components/GlobalControls/globalControls.scss
@@ -29,3 +29,7 @@ $bp: vendor.$bp-namespace;
@include button-colors;
}
}
+
+.bp3-menu-item.community-menu-item {
+ align-items: center;
+}
\ No newline at end of file
diff --git a/client/components/Menu/MenuItem.tsx b/client/components/Menu/MenuItem.tsx
index 117075e31a..96f15668bf 100644
--- a/client/components/Menu/MenuItem.tsx
+++ b/client/components/Menu/MenuItem.tsx
@@ -4,7 +4,7 @@ import { Classes, Icon } from '@blueprintjs/core';
import classNames from 'classnames';
import * as RK from 'reakit/Menu';
-import { Menu } from './Menu';
+import { Menu, type MenuProps } from './Menu';
import { MenuContext } from './menuContexts';
type SharedMenuItemProps = {
@@ -23,6 +23,7 @@ type SharedMenuItemProps = {
export type DisplayMenuItemProps = {
onDismiss: (...args: any[]) => unknown;
hasSubmenu: boolean;
+ submenuDirection?: 'left' | 'right';
} & SharedMenuItemProps;
const DisplayMenuItem = React.forwardRef((props: DisplayMenuItemProps, ref) => {
@@ -32,6 +33,7 @@ const DisplayMenuItem = React.forwardRef((props: DisplayMenuItemProps, ref) => {
className = '',
disabled = false,
hasSubmenu = false,
+ submenuDirection = 'right',
href,
icon = null,
onClick = null,
@@ -42,7 +44,11 @@ const DisplayMenuItem = React.forwardRef((props: DisplayMenuItemProps, ref) => {
...restProps
} = props;
- const label = hasSubmenu ? : rightElement;
+ const label = hasSubmenu ? (
+
+ ) : (
+ rightElement
+ );
const onClickWithHref = (evt) => {
if (onClick) {
@@ -93,18 +99,29 @@ export type MenuItemProps = {
text?: React.ReactNode;
children?: React.ReactNode;
dismissOnClick?: boolean;
- placement?: string;
+ placement?: MenuProps['placement'];
+ menuStyle?: object;
labelElement?: React.ReactNode;
} & SharedMenuItemProps;
export const MenuItem = React.forwardRef((props: MenuItemProps, ref) => {
- const { children = null, text, dismissOnClick = true, ...restProps } = props;
+ const {
+ children = null,
+ text,
+ dismissOnClick = true,
+ placement,
+ menuStyle,
+ ...restProps
+ } = props;
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dismissMenu' does not exist on type 'nul... Remove this comment to see the full error message
const { dismissMenu, parentMenu } = useContext(MenuContext);
if (children) {
+ const submenuPlacement = placement || undefined;
return (