diff --git a/wondrous-app/components/Common/RolePill/index.tsx b/wondrous-app/components/Common/RolePill/index.tsx index 329d15ab0a..586bb91f60 100644 --- a/wondrous-app/components/Common/RolePill/index.tsx +++ b/wondrous-app/components/Common/RolePill/index.tsx @@ -9,7 +9,9 @@ interface RolePillType { } const RolePill: React.FC = ({ roleName, onClick }) => ( - {`${getRoleEmoji(roleName)} ${roleName}`} + {`${getRoleEmoji(roleName)} ${ + roleName || 'no role' + }`} ); export default RolePill; diff --git a/wondrous-app/components/Common/SidebarEntityList/index.tsx b/wondrous-app/components/Common/SidebarEntityList/index.tsx index a1cf9208d6..b68169a11e 100644 --- a/wondrous-app/components/Common/SidebarEntityList/index.tsx +++ b/wondrous-app/components/Common/SidebarEntityList/index.tsx @@ -10,6 +10,7 @@ import PieChartIcon from 'components/Icons/Sidebar/pieChart.svg'; import ShowChartIcon from 'components/Icons/Sidebar/showChart.svg'; import StackIcon from 'components/Icons/Sidebar/stack.svg'; import StartIcon from 'components/Icons/Sidebar/star.svg'; +import PodIcon from 'components/Icons/Sidebar/pods.svg'; import { GET_TASKS_PER_TYPE, GET_TASKS_PER_TYPE_FOR_POD } from 'graphql/queries'; import { useRouter } from 'next/router'; import { ENTITIES_TYPES } from 'utils/constants'; @@ -48,7 +49,7 @@ const useSidebarData = () => { }; const link = orgBoard ? `/organization/${board?.orgData?.username}` : `/pod/${board?.podId}`; const taskCount = usePerTypeTaskCountForBoard(); - return { + const sidebarData = { handleOnClick, data: [ { @@ -93,12 +94,12 @@ const useSidebarData = () => { }, }, }, - - // { - // text: 'Pods', - // Icon: PodIcon, - // link: null, // link: not sure yet - // }, + !!orgBoard && { + text: 'Pods', + Icon: PodIcon, + link: `${link}/pods`, + count: board?.orgData?.podCount, + }, ], }, { @@ -140,6 +141,7 @@ const useSidebarData = () => { }, ], }; + return sidebarData; }; const location = () => { diff --git a/wondrous-app/components/Settings/Members/MembersTableRow/helpers.ts b/wondrous-app/components/Settings/Members/MembersTableRow/helpers.ts index a12f1f13a0..df5ffa9275 100644 --- a/wondrous-app/components/Settings/Members/MembersTableRow/helpers.ts +++ b/wondrous-app/components/Settings/Members/MembersTableRow/helpers.ts @@ -54,7 +54,7 @@ export const getRoleEmoji = (role) => { export const getRoleColor = (role) => { // role is either the role object or rolename if (!role) { - return ''; + return ROLE_COLORS_AND_EMOJIS[ROLES.NO_ROLE].color; } let correspondingRoleKey; if (typeof role === 'string') { diff --git a/wondrous-app/components/organization/pods/PodItem/index.tsx b/wondrous-app/components/organization/pods/PodItem/index.tsx new file mode 100644 index 0000000000..19cce070e9 --- /dev/null +++ b/wondrous-app/components/organization/pods/PodItem/index.tsx @@ -0,0 +1,51 @@ +import RolePill from 'components/Common/RolePill'; +import PodIcon from 'components/Icons/podIcon'; +import MemberIcon from 'components/Icons/Sidebar/people.svg'; +import { useOrgBoard } from 'utils/hooks'; +import { + PodDescriptionText, + PodItemContainer, + PodItemContributorsCount, + PodItemDetails, + PodItemDetailsContainer, + PodItemIconWrapper, + PodItemStats, + PodItemStatsContainer, + PodNameText, +} from './styles'; + +const PodItem = (props) => { + const { podData } = props; + + const { userPermissionsContext } = useOrgBoard() || {}; + + const podId = podData?.id; + const bgColor = podData?.color; + const podName = podData?.name; + const podDescription = podData?.description; + const contributorCount = podData?.contributorCount || 0; + const role = userPermissionsContext?.podRoles[podId]; + + return ( + + + + + + + {podName} + {!!podDescription && {podDescription}} + + + + + + {contributorCount} + + + + + ); +}; + +export default PodItem; diff --git a/wondrous-app/components/organization/pods/PodItem/styles.tsx b/wondrous-app/components/organization/pods/PodItem/styles.tsx new file mode 100644 index 0000000000..e9ff544d99 --- /dev/null +++ b/wondrous-app/components/organization/pods/PodItem/styles.tsx @@ -0,0 +1,86 @@ +import { Typography } from '@mui/material'; +import styled from 'styled-components'; +import palette from 'theme/palette'; + +export const PodItemContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + gap: 65px; + background: ${palette.background.default}; + transition: background 0.2s ease-in-out; + border-radius: 6px; + padding: 20px; + + &:hover { + background: ${palette.grey95}; + } +`; + +export const PodItemDetailsContainer = styled.div` + display: flex; + align-items: center; + gap: 16px; + max-width: 400px; + overflow: hidden; +`; + +export const PodItemDetails = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; + +export const PodNameText = styled(Typography)` + && { + color: ${palette.blue20}; + font-weight: 700; + font-size: 15px; + } +`; + +export const PodDescriptionText = styled(Typography)` + && { + color: ${palette.grey250}; + font-size: 14px; + max-width: 260px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +`; + +export const PodItemIconWrapper = styled.div` + padding: 10px; + border-radius: 1000px; + background: ${({ bgColor }) => bgColor || palette.black85}; + display: flex; +`; + +export const PodItemStatsContainer = styled.div` + display: flex; + align-items: center; + gap: 16px; + + p { + font-weight: 500; + font-size: 13px; + } +`; + +export const PodItemStats = styled.div` + display: flex; + align-items: center; + gap: 8px; + padding: 4px 6px; + border-radius: 4px; + background: ${palette.grey87}; +`; + +export const PodItemContributorsCount = styled(Typography)` + && { + color: ${palette.grey250}; + font-size: 13px; + font-weight: 500; + } +`; diff --git a/wondrous-app/components/organization/pods/constants.tsx b/wondrous-app/components/organization/pods/constants.tsx new file mode 100644 index 0000000000..bd7bb73fc6 --- /dev/null +++ b/wondrous-app/components/organization/pods/constants.tsx @@ -0,0 +1,5 @@ +export enum PodView { + ALL_PODS = 0, + PODS_USER_IS_MEMBER_OF = 1, + PODS_USER_IS_NOT_MEMBER_OF = 2, +} diff --git a/wondrous-app/components/organization/pods/index.tsx b/wondrous-app/components/organization/pods/index.tsx new file mode 100644 index 0000000000..be9a113a21 --- /dev/null +++ b/wondrous-app/components/organization/pods/index.tsx @@ -0,0 +1,191 @@ +import Link from 'next/link'; +import { useCallback, useEffect, useState } from 'react'; +import { useQuery } from '@apollo/client'; + +import { useMe } from 'components/Auth/withAuth'; + +import { useOrgBoard } from 'utils/hooks'; +import { capitalize } from 'utils/common'; +import { ENTITIES_TYPES, PERMISSIONS } from 'utils/constants'; +import { GET_ORG_PODS, GET_USER_PODS } from 'graphql/queries'; +import { parseUserPermissionContext } from 'utils/helpers'; + +import PlusIcon from 'components/Icons/plus'; +import { CreateEntity } from 'components/CreateEntity'; +import { + CreateNewPodButton, + CreateNewPodButtonText, + CreateNewPodButtonWrapper, + CreateNewPodIconWrapper, + PodHeadline, + PodItemWrapper, + PodsContainer, + PodsList, + SearchPods, + StyledTab, + StyledTabs, + TabLabelContainer, + TabLabelCount, + TabLabelText, +} from './styles'; + +import PodItem from './PodItem'; +import { PodView } from './constants'; + +const TabLabel = ({ label, count, isActive }) => ( + + {label} + {!!count && {count}} + +); + +const Pods = (props) => { + const { orgData } = props; + + const [activePodView, setActivePodView] = useState(PodView.ALL_PODS); + const [activePodsList, setActivePodsList] = useState([]); + const [showCreatePodModal, setShowCreatePodModal] = useState(false); + + const user = useMe(); + const { userPermissionsContext } = useOrgBoard() || {}; + const permissions = parseUserPermissionContext({ + userPermissionsContext, + orgId: orgData?.id, + }); + const { data: orgPodsData } = useQuery(GET_ORG_PODS, { + fetchPolicy: 'network-only', + variables: { + orgId: orgData?.id, + }, + }); + const { data: userPodsData } = useQuery(GET_USER_PODS, { + fetchPolicy: 'network-only', + variables: { + userId: user?.id, + }, + }); + + const orgName = orgData?.name || orgData?.username; + const orgPods = orgPodsData?.getOrgPods; + const userPods = userPodsData?.getUserPods; + const orgPodsUserIsIn = userPods?.filter((pod) => pod.org?.id === orgData?.id); + const orgPodsUserIsNotIn = orgPods?.filter( + (pod) => !orgPodsUserIsIn?.find((podUserIsIn) => podUserIsIn.id === pod.id) + ); + const canUserCreatePods = + permissions.includes(PERMISSIONS.FULL_ACCESS) || permissions.includes(PERMISSIONS.MANAGE_POD); + + const getActivePodsList = useCallback(() => { + if (activePodView === PodView.ALL_PODS) { + return orgPods; + } + if (activePodView === PodView.PODS_USER_IS_MEMBER_OF) { + return orgPodsUserIsIn; + } + if (activePodView === PodView.PODS_USER_IS_NOT_MEMBER_OF) { + return orgPodsUserIsNotIn; + } + return []; + }, [activePodView, orgPods?.length, orgPodsUserIsIn?.length, orgPodsUserIsNotIn?.length]); + + useEffect(() => { + const activePodsList = getActivePodsList(); + setActivePodsList(activePodsList); + }, [activePodView, orgPods?.length, orgPodsUserIsIn?.length, orgPodsUserIsNotIn?.length]); + + const handleActiveTabChange = (_, newValue: number) => { + setActivePodView(newValue); + }; + + const handleSearchPods = useCallback( + (ev) => { + const searchValue = ev.target.value?.toLowerCase(); + const activePodsList = getActivePodsList(); + + if (searchValue) { + const filteredPods = activePodsList.filter( + (pod) => + pod.name?.toLowerCase().includes(searchValue) || + pod.description?.toLowerCase()?.includes(searchValue) || + pod.id?.includes(searchValue) + ); + setActivePodsList(filteredPods); + } else { + setActivePodsList(activePodsList); + } + }, + [activePodView, orgPods?.length, orgPodsUserIsIn?.length, orgPodsUserIsNotIn?.length] + ); + + const handleOpenCreatePodModal = () => { + setShowCreatePodModal(true); + }; + + const handleCloseCreatePodModal = () => { + setShowCreatePodModal(false); + }; + + return ( + + Pods in {capitalize(orgName)} + + + } + /> + + } + /> + + } + /> + + + + + {canUserCreatePods && ( + + + + + + Create new pod + + + )} + + + {activePodsList?.length && + activePodsList?.map((podData) => ( + + + + + + ))} + + + + + ); +}; + +export default Pods; diff --git a/wondrous-app/components/organization/pods/styles.tsx b/wondrous-app/components/organization/pods/styles.tsx new file mode 100644 index 0000000000..6b3eabac80 --- /dev/null +++ b/wondrous-app/components/organization/pods/styles.tsx @@ -0,0 +1,188 @@ +import styled from 'styled-components'; +import Typography from '@mui/material/Typography'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Button from '@mui/material/Button'; +import Input from '@mui/material/Input'; +import InputAdornment from '@mui/material/InputAdornment'; + +import SearchIcon from 'components/Icons/search'; + +import typography from 'theme/typography'; +import palette from 'theme/palette'; + +export const PodsContainer = styled.div` + padding: 120px 0; + margin: 0 auto; + max-width: 720px; +`; + +export const PodHeadline = styled(Typography)` + && { + color: ${palette.white}; + font-weight: 700; + font-family: ${typography.fontFamily}; + font-size: 24px; + } +`; + +export const StyledTabs = styled(Tabs)` + && { + color: ${palette.white}; + ${({ withMargin = true }) => withMargin && 'margin: 24px auto;'}; + width: 100%; + } + .MuiTabs-flexContainer { + justify-content: start; + ${({ withBorder = true }) => withBorder && `border-bottom: 2px solid ${palette.black92};`}; + gap: 20px; + } + .MuiTab-textColorInherit { + opacity: 1; + } + .MuiTabs-indicator { + background: linear-gradient( + 270deg, + ${palette.blue20} 2.13%, + ${palette.highlightPurple} 48.52%, + ${palette.highlightBlue} 100% + ); + } +`; + +export const StyledTab = styled(Tab)` + && { + font-family: ${typography.fontFamily}; + font-size: 14px; + font-weight: 500; + padding: 7px 4px; + color: ${({ isActive }) => (isActive ? palette.white : palette.grey51)}; + } +`; + +export const TabLabelContainer = styled.div` + display: flex; + align-items: center; + gap: 10px; +`; + +export const TabLabelText = styled(Typography)` + && { + color: ${({ isActive }) => (isActive ? palette.white : palette.grey51)}; + font-weight: 500; + font-family: ${typography.fontFamily}; + font-size: 14px; + } +`; + +export const TabLabelCount = styled(TabLabelText)` + && { + background: ${({ isActive }) => (isActive ? palette.grey87 : palette.grey87)}; + padding: 2px; + border-radius: 4px; + } +`; + +export const SearchPods = styled(({ ...props }) => ( + + + + } + /> +))` + && { + height: 40px; + background: ${palette.black101}; + color: ${palette.grey250}; + width: 100%; + padding: 0 10px; + margin: 10px 0 31px 0; + border-radius: 6px; + font-size: 14px; + } +`; + +export const CreateNewPodButtonWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + padding: 14px 0; + background: ${palette.black92}; + border-radius: 6px; +`; + +export const CreateNewPodButton = styled(Button)` + && { + display: flex; + align-items: center; + gap: 8px; + background: ${palette.grey87}; + border-radius: 6px; + padding: 8px; + transition: background 0.2s ease-in-out; + + &:hover { + background: ${palette.grey88}; + } + } +`; + +export const CreateNewPodIconWrapper = styled.div` + height: 24px; + width: 24px; + background: ${palette.background.default}; + border-radius: 1000px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + + svg path { + fill: ${palette.blue20}; + } + + &::before { + content: ''; + display: block; + height: 100%; + width: 100%; + position: absolute; + top: 0; + left: 0; + background: linear-gradient( + 232.77deg, + ${palette.highlightBlue} 18.39%, + ${palette.highlightPurple} 86.24%, + ${palette.blue20} 161.54% + ); + mask: linear-gradient(${palette.white} 0 0) content-box, linear-gradient(${palette.white} 0 0); + mask-composite: xor; + mask-composite: exclude; + padding: 2px; + border-radius: 1000px; + } +`; + +export const CreateNewPodButtonText = styled(Typography)` + && { + font-family: ${typography.fontFamily}; + font-size: 14px; + font-weight: 500; + color: ${palette.white}; + } +`; + +export const PodsList = styled.div` + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 24px; +`; + +export const PodItemWrapper = styled.a` + text-decoration: none; +`; diff --git a/wondrous-app/pages/organization/[username]/pods.tsx b/wondrous-app/pages/organization/[username]/pods.tsx new file mode 100644 index 0000000000..bca3d6cce8 --- /dev/null +++ b/wondrous-app/pages/organization/[username]/pods.tsx @@ -0,0 +1,43 @@ +import { useRouter } from 'next/router'; +import { useQuery } from '@apollo/client'; + +import Pods from 'components/organization/pods'; +import MobileComingSoonModal from 'components/Onboarding/MobileComingSoonModal'; +import EntitySidebar from 'components/Common/SidebarEntity'; +import { withAuth } from 'components/Auth/withAuth'; + +import { GET_USER_PERMISSION_CONTEXT } from 'graphql/queries'; + +import { useGetOrgFromUsername, useIsMobile } from 'utils/hooks'; +import { OrgBoardContext } from 'utils/contexts'; + +function PodsInOrgPage() { + const router = useRouter(); + const isMobile = useIsMobile(); + + const { username } = router.query; + + const orgData = useGetOrgFromUsername(username); + const { data: userPermissionsContext } = useQuery(GET_USER_PERMISSION_CONTEXT, { + fetchPolicy: 'cache-and-network', + }); + + return ( + + {isMobile ? : null} + + + + + ); +} + +export default withAuth(PodsInOrgPage); diff --git a/wondrous-app/utils/common.tsx b/wondrous-app/utils/common.tsx index a8031bd2f9..f3ce0af807 100644 --- a/wondrous-app/utils/common.tsx +++ b/wondrous-app/utils/common.tsx @@ -128,4 +128,9 @@ export const parseLinks = (links) => { }; }; -export const capitalize = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1); +export const capitalize = (str: string): string => { + if (!str) { + return ''; + } + return str.charAt(0).toUpperCase() + str.slice(1); +}; diff --git a/wondrous-app/utils/constants.tsx b/wondrous-app/utils/constants.tsx index fd87ddc17f..0fae6e4c32 100644 --- a/wondrous-app/utils/constants.tsx +++ b/wondrous-app/utils/constants.tsx @@ -705,6 +705,7 @@ export const ROLES = { OWNER: 'owner', CORE_TEAM: 'core team', CONTRIBUTOR: 'contributor', + NO_ROLE: 'no role', DEFAULT: 'default', // this is for any role other than the above }; @@ -721,6 +722,10 @@ export const ROLE_COLORS_AND_EMOJIS = { color: palette.highlightOrange, emoji: '✨', }, + [ROLES.NO_ROLE]: { + color: palette.grey87, + emoji: '', + }, [ROLES.DEFAULT]: { color: palette.highlightBlue, emoji: '🐦',