Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions client/components/GlobalControls/CreatePubButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,8 @@ const CreatePubButton = () => {
evt.preventDefault();
}}
ref={formRef}
// only relevant in dev
style={{ display: 'flex', alignItems: 'center', gap: '10px' }}
style={{ position: 'relative', display: 'flex', alignItems: 'center' }}
>
<Altcha ref={altchaRef} />
<Honeypot name="description" />
<GlobalControlsButton
onClick={handleCreatePub}
loading={isLoading}
Expand All @@ -71,6 +68,19 @@ const CreatePubButton = () => {
desktop={{ text: 'Create Pub' }}
mobile={{ icon: 'pubDocNew' }}
/>
<div
style={{
position: 'absolute',
top: '100%',
left: 0,
display: 'flex',
gap: 6,
paddingTop: 4,
}}
>
<Altcha ref={altchaRef} />
<Honeypot name="description" />
</div>
</form>
);
};
Expand Down
48 changes: 36 additions & 12 deletions client/components/GlobalControls/GlobalControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,24 @@ const GlobalControls = (props: Props) => {
const canCreatePub = loggedIn && (!hideCreatePubButton || canManage);
return (
<>
{canSelectCommunityForDevelopment() && (
<DevCommunitySwitcherMenu
disclosure={
<GlobalControlsButton
mobileOrDesktop={{
icon: pubPubIcons.community,
rightIcon: 'caret-down',
}}
/>
}
/>
)}
{loginData.isSuperAdmin && (
<GlobalControlsButton
href="/superadmin"
mobileOrDesktop={{ text: 'Superadmin' }}
/>
)}
{canCreatePub && <CreatePubButton />}
{renderSearch()}
{canView && renderDashboardMenu()}
Expand All @@ -99,6 +117,24 @@ const GlobalControls = (props: Props) => {
if (isBasePubPub) {
return (
<>
{canSelectCommunityForDevelopment() && (
<DevCommunitySwitcherMenu
disclosure={
<GlobalControlsButton
mobileOrDesktop={{
icon: pubPubIcons.community,
rightIcon: 'caret-down',
}}
/>
}
/>
)}
{loginData.isSuperAdmin && (
<GlobalControlsButton
href="/superadmin"
mobileOrDesktop={{ text: 'Superadmin' }}
/>
)}
{/* <GlobalControlsButton
href="https://www.knowledgefutures.org/pubpub/"
mobileOrDesktop={{ text: 'PubPub Platform (new!)' }}
Expand All @@ -119,18 +155,6 @@ const GlobalControls = (props: Props) => {

return (
<div className="global-controls-component">
{canSelectCommunityForDevelopment() && (
<DevCommunitySwitcherMenu
disclosure={
<GlobalControlsButton
mobileOrDesktop={{
icon: pubPubIcons.community,
rightIcon: 'caret-down',
}}
/>
}
/>
)}
{renderItemsVisibleFromCommunity()}
{renderBasePubPubLinks()}
{renderUserMenuOrLogin()}
Expand Down
181 changes: 177 additions & 4 deletions client/components/GlobalControls/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 (
<img
src={community.avatar}
alt=""
style={{
width: size,
height: size,
borderRadius: 3,
objectFit: 'cover',
flexShrink: 0,
}}
/>
);
}
return (
<div
style={{
width: size,
height: size,
borderRadius: 3,
backgroundColor: bgColor,
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: size * 0.55,
fontWeight: 600,
flexShrink: 0,
}}
>
{initial}
</div>
);
};

const SkeletonRow = () => (
<li style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 7px' }}>
<div className={Classes.SKELETON} style={{ width: 24, height: 24, borderRadius: 3 }} />
<div style={{ flex: 1 }}>
<div
className={Classes.SKELETON}
style={{ width: 120, height: 12, marginBottom: 4, borderRadius: 2 }}
/>
<div className={Classes.SKELETON} style={{ width: 90, height: 10, borderRadius: 2 }} />
</div>
</li>
);

const YourCommunitiesSection = ({ communities }: { communities: UserCommunity[] | null }) => {
const { locationData } = usePageContext();

// Hide entirely if no communities
if (communities && communities.length === 0) return null;

return (
<>
<li
style={{
fontWeight: 600,
fontSize: 12,
color: '#555',
padding: '10px 7px 5px',
marginTop: 4,
borderTop: '1px solid rgba(16, 22, 26, 0.15)',
// borderBottom: '1px solid rgba(16, 22, 26, 0.1)',
}}
>
Your Communities
</li>
<div style={{ maxHeight: 'min(40vh, 300px)', overflowY: 'auto' }}>
{!communities ? (
<>
<SkeletonRow />
<SkeletonRow />
<SkeletonRow />
</>
) : (
communities.map((community) => (
<MenuItem
key={community.id}
href={getCommunityUrl(community, locationData)}
icon={<CommunityAvatar community={community} />}
className="community-menu-item"
menuStyle={{
display: 'flex',
alignItems: 'center',
}}
text={
<div style={{ lineHeight: 1.3, maxWidth: 200 }}>
<div
style={{
fontWeight: 500,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{community.title}
</div>
<div
style={{
fontSize: 11,
opacity: 0.65,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{getCommunityDisplayUrl(community)}
</div>
</div>
}
/>
))
)}
</div>
</>
);
};

const UserMenu = (props: Props) => {
const { loginData } = props;
const [communities, setCommunities] = useState<UserCommunity[] | null>(null);
const [hasFetched, setHasFetched] = useState(false);

const handleVisibleChange = useCallback(
(visible: boolean) => {
if (visible && !hasFetched) {
setHasFetched(true);
apiFetch
.get<UserCommunity[]>('/api/users/communities')
.then(setCommunities)
.catch(() => setCommunities([]));
}
},
[hasFetched],
);

return (
<Menu
aria-label="User menu"
placement="bottom-end"
// The z-index of the PubHeaderFormatting is 19
menuStyle={{ zIndex: 20 }}
menuStyle={{ zIndex: 20, width: 245 }}
disclosure={(disclosureProps) => renderDisclosure(loginData, disclosureProps)}
onVisibleChange={handleVisibleChange}
>
<MenuItem
href={`/user/${loginData.slug}`}
Expand All @@ -79,6 +251,7 @@ const UserMenu = (props: Props) => {
/>
<MenuItem href="/legal/settings" text="Privacy &amp; Account Settings" />
<MenuItem onClick={handleLogout} text="Logout" />
<YourCommunitiesSection communities={communities} />
</Menu>
);
};
Expand Down
4 changes: 4 additions & 0 deletions client/components/GlobalControls/globalControls.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ $bp: vendor.$bp-namespace;
@include button-colors;
}
}

.bp3-menu-item.community-menu-item {
align-items: center;
}
Loading
Loading