Skip to content

Commit

Permalink
feat: Create the User Popup for the cluster map (#129)
Browse files Browse the repository at this point in the history
* feat: Creating the Cluster Popup and rework ClusterMap to be more sexy to implement 🔞
* feat: Release the UserPopup features on ClusterMaps with docs
  • Loading branch information
42atomys authored May 28, 2022
1 parent 9e3a4e0 commit 6bdc944
Show file tree
Hide file tree
Showing 23 changed files with 596 additions and 189 deletions.
2 changes: 1 addition & 1 deletion api/graphs/api.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ type Location {
type LocationConnection {
totalCount: Int!
pageInfo: PageInfo!
edges: [LocationEdge]!
edges: [LocationEdge!]!
}

type LocationEdge {
Expand Down
2 changes: 1 addition & 1 deletion web/ui/src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const Avatar = ({
className={classNames(
className,
rounded ? 'rounded-full' : 'rounded',
'bg-clip-border bg-center bg-cover',
'bg-clip-border bg-center bg-cover bg-slate-900/30',
sizeClasses[size]
)}
/>
Expand Down
12 changes: 8 additions & 4 deletions web/ui/src/components/Badge/LocationBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import Emoji from '@components/Emoji';
import { Location } from '@graphql.d';
import classNames from 'classnames';
import { Location } from 'types/globals';
import { Badge } from './Badge';

export const LocationBadge = (location: Location) => {
const isConnected = location.host ? true : false;
export const LocationBadge = ({
location,
}: {
location: Partial<Location> | null | undefined;
}) => {
const isConnected = location?.identifier ? true : false;

return (
<Badge color={isConnected ? 'green' : 'gray'}>
Expand All @@ -15,7 +19,7 @@ export const LocationBadge = (location: Location) => {
)}
></span>
<span className="flex flex-row justify-center items-center text-sm mx-1">
{isConnected ? location.host : 'Offline'}
{isConnected ? location?.identifier : 'Offline'}
</span>
<Emoji
emoji="🇫🇷"
Expand Down
80 changes: 80 additions & 0 deletions web/ui/src/components/ClusterMap/ClusterContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import Loader from '@components/Loader';
import useSidebar from '@components/Sidebar';
import { UserPopup, PopupConsumer, PopupProvider } from '@components/UserPopup';
import { useClusterViewQuery, User } from '@graphql.d';
import { isFirstLoading } from '@lib/apollo';
import { ClusterSidebar } from '../../containers/clusters/ClusterSidebar';
import { ClusterContainerComponent } from './types';

/**
* ClusterContainer component is used to display the cluster map and the cluster
* sidebar. It is used in the cluster page ONLY. This component give facilities
* to display the cluster map with popup management.
*
* Children function parameters:
* - locations: the locations of the cluster and campus
* - showPopup: a function to show the popup
* - hidePopup: a function to hide the popup
*
* Example:
* ```
<ClusterContainer campus="Paris" cluster={cluster}>
{({ locations, showPopup, hidePopup }) => (
<div></div>
)}
</ClusterContainer>
* ```
*
* You can see a fully fonctionnal example on Paris cluster map at
* `src/pages/clusters/paris/[cluster].tsx`
*/
export const ClusterContainer: ClusterContainerComponent = ({
campus,
cluster,
children,
}) => {
const { SidebarProvider, PageContainer, PageContent } = useSidebar();
const { data, networkStatus, error } = useClusterViewQuery({
variables: { campusName: campus, identifierPrefix: cluster },
});

return (
<SidebarProvider>
<PageContainer>
<PopupProvider>
<>
<ClusterSidebar campus="paris" cluster={cluster as string} />
<PageContent
className={
'p-2 flex-1 flex justify-center min-h-screen items-center'
}
>
{isFirstLoading(networkStatus) && <Loader />}
{error && <div>Error!</div>}

{!isFirstLoading(networkStatus) && data && (
<PopupConsumer>
{([state, dispatch]) => (
<>
{children({
locations: data.locationsByCluster,
showPopup: (s) => dispatch('SHOW_POPUP', s),
hidePopup: () => dispatch('HIDE_POPUP', null),
})}
<UserPopup
user={state.user as User}
location={state.location}
position={state.position}
onClickOutside={() => dispatch('HIDE_POPUP', null)}
/>
</>
)}
</PopupConsumer>
)}
</PageContent>
</>
</PopupProvider>
</PageContainer>
</SidebarProvider>
);
};
68 changes: 48 additions & 20 deletions web/ui/src/components/ClusterMap/ClusterMap.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import Avatar from '@components/Avatar';
import classNames from 'classnames';
import { Children } from 'react';
import { MapLocation } from './types';

/**
* ClusterMap component is used to display a cluster map with a table style
* like Paris maps.
*/
export const ClusterMap = ({
children,
}: {
Expand All @@ -16,16 +21,34 @@ export const ClusterMap = ({
);
};

/**
* ClusterWorkspaceWithUser component is used to display a workspace with
* user avatar and interaction strategy when the user is actually connected to
* this workspace.
* and identifier in a `ClusterRow`
*/
export const ClusterWorkspaceWithUser = ({
displayText,
location,
onMouseEnter,
onMouseLeave,
onClick,
}: {
identifier: string;
displayText?: string;
location: {
identifier: string;
user: { isFollowing: boolean; duoLogin: string };
};
location: MapLocation;
onMouseEnter?: (
e: React.MouseEvent<HTMLDivElement>,
location: MapLocation
) => void;
onMouseLeave?: (
e: React.MouseEvent<HTMLDivElement>,
location: MapLocation
) => void;
onClick?: (
e: React.MouseEvent<HTMLDivElement>,
location: MapLocation
) => void;
}) => {
return (
<div
Expand All @@ -35,6 +58,9 @@ export const ClusterWorkspaceWithUser = ({
? 'cursor-pointer bg-blue-300/30 dark:bg-blue-700/30 text-blue-500'
: 'cursor-pointer bg-emerald-300/30 dark:bg-emerald-700/30 text-emerald-500'
)}
onClick={(e) => onClick && onClick(e, location)}
onMouseEnter={(e) => onMouseEnter && onMouseEnter(e, location)}
onMouseLeave={(e) => onMouseLeave && onMouseLeave(e, location)}
>
<span className="mb-1">
<Avatar login={location.user.duoLogin} rounded={false} size="md" />
Expand All @@ -43,30 +69,20 @@ export const ClusterWorkspaceWithUser = ({
</div>
);
};

/**
* ClusterWorkspace component is used to display a workspace with compouter icon
* and identifier in a `ClusterRow`
*/
export const ClusterWorkspace = ({
identifier,
displayText,
connected,
friend,
}: {
identifier: string;
displayText?: string;
connected?: boolean;
friend?: boolean;
}) => {
return (
<div
className={classNames(
'flex flex-1 flex-col justify-center items-center m-0.5 rounded text-slate-500',
!connected && 'bg-slate-200/30 dark:bg-slate-900/30',
connected &&
!friend &&
'cursor-pointer bg-emerald-300/30 dark:bg-emerald-700/30 text-emerald-500',
connected &&
friend &&
'cursor-pointer bg-blue-300/30 dark:bg-blue-700/30 text-blue-500'
)}
>
<div className="flex flex-1 flex-col justify-center items-center m-0.5 rounded text-slate-500">
<span className="opacity-50">
<i className="fa-light fa-computer"></i>
</span>
Expand All @@ -75,12 +91,20 @@ export const ClusterWorkspace = ({
);
};

/**
* ClusterPillar component is used to display a simple pillar in a `ClusterRow`
* Principally used to display a pillar or something cannot be used as path.
*/
export const ClusterPillar = () => {
return (
<div className="flex flex-1 flex-col justify-center items-center m-0.5 rounded bg-slate-200 dark:bg-slate-900"></div>
);
};

/**
* ClusterPillar component is used to display an empty space in a `ClusterRow`.
* Principally used to display a path in the cluster.
*/
export const ClusterEmpty = ({ displayText }: { displayText?: string }) => {
return (
<div className="flex flex-1 flex-col justify-center items-center m-0.5 rounded bg-transparent">
Expand All @@ -89,6 +113,10 @@ export const ClusterEmpty = ({ displayText }: { displayText?: string }) => {
);
};

/**
* ClusterRow component is used to display a row in cluster map row with a table
* style like Paris maps.
*/
export const ClusterRow = ({
displayText,
children,
Expand Down
2 changes: 2 additions & 0 deletions web/ui/src/components/ClusterMap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export {
} from './ClusterMap';

export { extractNode, extractandRemoveNode } from './utils';

export type { MapLocation } from './types';
26 changes: 25 additions & 1 deletion web/ui/src/components/ClusterMap/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import type { Actions, PayloadOf } from '@components/UserPopup';
import type { ClusterViewQuery } from '@graphql.d';
import { NonNullable } from 'types/utils';

// ClusterMap.tsx
export type MapLocation = NonNullable<
ClusterViewQuery['locationsByCluster']['edges'][number]['node']
>;

type Connection = {
edges: Array<{ node?: { identifier: string } | null } | null>;
edges: Array<{ node?: Pick<MapLocation, 'identifier'> | null } | null>;
};

type NodeFinderFunc = <T extends Connection>(
Expand All @@ -11,3 +20,18 @@ type NodeIndexFinderFunc = <T extends Connection>(
connection: T,
identifier: string
) => number | -1;

// ClusterContainer.tsx
type ClusterContainerChildrenProps = {
locations: ClusterViewQuery['locationsByCluster'];
showPopup: (s: PayloadOf<Actions, 'SHOW_POPUP'>) => void;
hidePopup: () => void;
};

type ClusterContainerProps = {
campus: 'Paris';
cluster: 'e1' | 'e2' | 'e3';
children: (props: ClusterContainerChildrenProps) => JSX.Element;
};

type ClusterContainerComponent = (props: ClusterContainerProps) => JSX.Element;
2 changes: 2 additions & 0 deletions web/ui/src/components/ClusterMap/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { NodeFinderFunc, NodeIndexFinderFunc } from './types';

/**
* findIndexForNode will extract the index of the node object from a graphql
* query result object. (for example you can see the request locationsByCluster)
Expand Down
86 changes: 86 additions & 0 deletions web/ui/src/components/UserCard/DropDownMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
useCreateFriendshipMutation,
useDeleteFriendshipMutation,
} from '@graphql.d';
import { Menu, Transition } from '@headlessui/react';
import classNames from 'classnames';
import { Fragment } from 'react';
import { DropdownMenuComponent } from './types';

const DropdownMenu: DropdownMenuComponent = ({
userID,
isFriend = false,
buttonAlwaysShow = false,
refetchQueries = [],
}) => {
const [deleteFriendship] = useDeleteFriendshipMutation();
const [addFriendship] = useCreateFriendshipMutation();

return (
<div className="text-right absolute top-2 right-2">
<Menu as="div" className="relative inline-block text-left">
<Menu.Button className="inline-flex justify-center w-full">
<i
className={classNames(
'fa-light fa-ellipsis-vertical w-[18px] h-[18px] text-lg rounded-full p-2 hover:text-indigo-800 dark:hover:text-indigo-200 hover:bg-indigo-500/20',
buttonAlwaysShow ? 'visible' : 'invisible group-hover:visible'
)}
></i>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className={classNames(
'absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-slate-900 divide-y divide-gray-100 dark:divide-slate-800 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none',
buttonAlwaysShow ? 'visible' : 'invisible group-hover:visible'
)}
>
<div className="px-1 py-1">
{!isFriend && (
<Menu.Item>
<button
onClick={() => {
addFriendship({
variables: { userID: userID },
refetchQueries: refetchQueries,
});
}}
className="hover:bg-indigo-500 hover:text-white text-indigo-500 group flex rounded-md items-center w-full px-2 py-2 text-sm"
>
<i className="fa-light fa-user-plus pr-2"></i>
<span>Add Friend</span>
</button>
</Menu.Item>
)}
{isFriend && (
<Menu.Item>
<button
onClick={() => {
deleteFriendship({
variables: { userID: userID },
refetchQueries: refetchQueries,
});
}}
className="hover:bg-red-500 hover:text-white text-red-500 group flex rounded-md items-center w-full px-2 py-2 text-sm"
>
<i className="fa-light fa-user-xmark pr-2"></i>
<span>Remove Friend</span>
</button>
</Menu.Item>
)}
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
);
};

export default DropdownMenu;
Loading

0 comments on commit 6bdc944

Please sign in to comment.