diff --git a/packages/commonwealth/client/scripts/styles/mixins/table.scss b/packages/commonwealth/client/scripts/styles/mixins/table.scss new file mode 100644 index 00000000000..7e4190c3f76 --- /dev/null +++ b/packages/commonwealth/client/scripts/styles/mixins/table.scss @@ -0,0 +1,33 @@ +@mixin table-cell { + .table-cell { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + + .user-info { + text-decoration: none; + display: flex; + align-items: center; + gap: 8px; + color: black; + + div { + display: flex; + justify-content: center; + align-items: center; + } + + :hover, + :focus, + :visited { + text-decoration: underline; + } + } + + &.text-right { + display: block; + text-align: right; + } + } +} diff --git a/packages/commonwealth/client/scripts/styles/shared.scss b/packages/commonwealth/client/scripts/styles/shared.scss index d688e998ea5..911ba05f143 100644 --- a/packages/commonwealth/client/scripts/styles/shared.scss +++ b/packages/commonwealth/client/scripts/styles/shared.scss @@ -8,6 +8,7 @@ @import 'mixins/inputs'; @import 'mixins/icons'; @import 'mixins/text'; +@import 'mixins/table'; @import 'utils'; // layout & global nav parameters diff --git a/packages/commonwealth/client/scripts/views/components/Profile/Profile.tsx b/packages/commonwealth/client/scripts/views/components/Profile/Profile.tsx index 985bf2651c8..316b7398963 100644 --- a/packages/commonwealth/client/scripts/views/components/Profile/Profile.tsx +++ b/packages/commonwealth/client/scripts/views/components/Profile/Profile.tsx @@ -139,7 +139,11 @@ const Profile = ({ userId }: ProfileProps) => { > {/* @ts-expect-error StrictNullChecks*/} - + @@ -151,7 +155,11 @@ const Profile = ({ userId }: ProfileProps) => {
{/* @ts-expect-error StrictNullChecks*/} - +
diff --git a/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivity.scss b/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivity.scss index a5ae8942b8a..54dfcb5303a 100644 --- a/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivity.scss +++ b/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivity.scss @@ -17,6 +17,7 @@ .CWTabsRow { align-items: center; + overflow: auto; .tab-header { display: flex; diff --git a/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivity.tsx b/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivity.tsx index 871b50593eb..b8462d0efb4 100644 --- a/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivity.tsx +++ b/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivity.tsx @@ -2,18 +2,14 @@ import React, { useState } from 'react'; import './ProfileActivity.scss'; +import { useFlag } from 'hooks/useFlag'; import type Comment from 'models/Comment'; import type Thread from 'models/Thread'; import type { IUniqueId } from 'models/interfaces'; import { CWTab, CWTabsRow } from '../component_kit/new_designs/CWTabs'; -import ProfileActivityContent from './ProfileActivityContent'; - -enum ProfileActivityType { - Addresses, - Comments, - Communities, - Threads, -} +import ProfileActivityContent, { + ProfileActivityType, +} from './ProfileActivityContent'; export type CommentWithAssociatedThread = Comment & { thread: Thread; @@ -22,13 +18,20 @@ export type CommentWithAssociatedThread = Comment & { type ProfileActivityProps = { comments: CommentWithAssociatedThread[]; threads: Thread[]; + isOwner: boolean | undefined; }; -const ProfileActivity = ({ comments, threads }: ProfileActivityProps) => { +const ProfileActivity = ({ + comments, + threads, + isOwner, +}: ProfileActivityProps) => { const [selectedActivity, setSelectedActivity] = useState( ProfileActivityType.Comments, ); + const referralsEnabled = useFlag('referrals'); + return (
@@ -52,6 +55,20 @@ const ProfileActivity = ({ comments, threads }: ProfileActivityProps) => { }} isSelected={selectedActivity === ProfileActivityType.Threads} /> + {referralsEnabled && ( + + Referrals +
5
+
+ } + onClick={() => { + setSelectedActivity(ProfileActivityType.Referrals); + }} + /> + )}
@@ -59,6 +76,7 @@ const ProfileActivity = ({ comments, threads }: ProfileActivityProps) => { option={selectedActivity} threads={threads} comments={comments} + isOwner={isOwner} />
diff --git a/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivityContent.tsx b/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivityContent.tsx index e0122f48914..10bc51380d3 100644 --- a/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivityContent.tsx +++ b/packages/commonwealth/client/scripts/views/components/Profile/ProfileActivityContent.tsx @@ -6,24 +6,28 @@ import type Thread from 'models/Thread'; import { CWText } from '../component_kit/cw_text'; import type { CommentWithAssociatedThread } from './ProfileActivity'; import ProfileActivityRow from './ProfileActivityRow'; +import ReferralsTab from './ReferralsTab'; -enum ProfileActivityType { +export enum ProfileActivityType { Addresses, Comments, Communities, Threads, + Referrals, } type ProfileActivityContentProps = { option: ProfileActivityType; threads: Thread[]; comments: CommentWithAssociatedThread[]; + isOwner: boolean | undefined; }; const ProfileActivityContent = ({ option, comments, threads, + isOwner, }: ProfileActivityContentProps) => { if (option === ProfileActivityType.Threads) { if (threads.length === 0) { @@ -49,6 +53,10 @@ const ProfileActivityContent = ({ ); } + if (option === ProfileActivityType.Referrals) { + return ; + } + const allActivities: Array = [ ...comments, ...threads, diff --git a/packages/commonwealth/client/scripts/views/components/Profile/ReferralsTab/ReferralsTab.scss b/packages/commonwealth/client/scripts/views/components/Profile/ReferralsTab/ReferralsTab.scss new file mode 100644 index 00000000000..65567e34a74 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/Profile/ReferralsTab/ReferralsTab.scss @@ -0,0 +1,57 @@ +@import '../../../../styles/shared.scss'; + +.ReferralsTab { + display: flex; + flex-direction: column; + gap: 24px; + + input, + svg { + cursor: pointer !important; + } + + input:read-only { + background-color: $neutral-50; + border-color: $neutral-200; + color: $neutral-400; + + &:hover { + border-color: $neutral-200; + } + + &:focus-within { + border-color: $neutral-200 !important; + box-shadow: none !important; + } + } + + table { + width: 100%; + @include table-cell; + } + + .referral-totals { + margin-right: 16px; + display: flex; + justify-content: flex-end; + gap: 8px; + align-items: center; + } + + .empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 176px 0; + + @include mediumSmallInclusive { + padding: 96px 0; + } + + .empty-state-text { + color: $neutral-400; + text-align: center; + } + } +} diff --git a/packages/commonwealth/client/scripts/views/components/Profile/ReferralsTab/ReferralsTab.tsx b/packages/commonwealth/client/scripts/views/components/Profile/ReferralsTab/ReferralsTab.tsx new file mode 100644 index 00000000000..d1ea219d006 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/Profile/ReferralsTab/ReferralsTab.tsx @@ -0,0 +1,186 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { useUserStore } from 'state/ui/user/user'; +import { saveToClipboard } from 'utils/clipboard'; + +import { APIOrderDirection } from 'helpers/constants'; +import { Avatar } from '../../Avatar'; +import { CWIcon } from '../../component_kit/cw_icons/cw_icon'; +import { CWText } from '../../component_kit/cw_text'; +import CWIconButton from '../../component_kit/new_designs/CWIconButton'; +import CWPopover, { + usePopover, +} from '../../component_kit/new_designs/CWPopover'; +import { CWTable } from '../../component_kit/new_designs/CWTable'; +import { CWTableColumnInfo } from '../../component_kit/new_designs/CWTable/CWTable'; +import { useCWTableState } from '../../component_kit/new_designs/CWTable/useCWTableState'; +import { CWTextInput } from '../../component_kit/new_designs/CWTextInput'; + +import './ReferralsTab.scss'; + +const fakeData = [ + { + user: { + name: 'cambell', + avatarUrl: + 'https://assets.commonwealth.im/794bb7a3-17d7-407a-b52e-2987501221b5.png', + userId: '128606', + address: 'address1', + }, + earnings: '5.3', + }, + { + user: { + name: 'adam', + avatarUrl: + 'https://assets.commonwealth.im/0847e7f5-4d96-4406-8f30-c3082fa2f27c.png', + userId: '135099', + address: 'address2', + }, + earnings: '1.9', + }, + { + user: { + name: 'mike', + avatarUrl: + 'https://assets.commonwealth.im/181e25ad-ce08-427d-8d3a-d290af3be44b.png', + userId: '158139', + address: 'address3', + }, + earnings: '0.1', + }, +]; + +const columns: CWTableColumnInfo[] = [ + { + key: 'member', + header: 'Member', + numeric: false, + sortable: true, + }, + + { + key: 'earnings', + header: 'Earnings', + numeric: true, + sortable: true, + }, +]; + +interface ReferralsTabProps { + isOwner: boolean | undefined; +} + +const ReferralsTab = ({ isOwner }: ReferralsTabProps) => { + const user = useUserStore(); + const popoverProps = usePopover(); + + const tableState = useCWTableState({ + columns, + initialSortColumn: 'earnings', + initialSortDirection: APIOrderDirection.Desc, + }); + + // TODO: replace with actual invite link from backend in upcoming PR + const inviteLink = 'https://commonwealth.im/~/invite/774037=89defcb8'; + + const handleCopy = () => { + saveToClipboard(inviteLink, true).catch(console.error); + }; + + const isCurrentUser = user.isLoggedIn && isOwner; + + return ( +
+ {isCurrentUser && ( + } + /> + )} + + {fakeData.length > 0 ? ( + <> + ({ + ...item, + member: { + sortValue: item.user.name.toLowerCase(), + customElement: ( +
+ + +

{item.user.name}

+ +
+ ), + }, + earnings: { + sortValue: item.earnings, + customElement: ( +
+ USD {item.earnings} +
+ ), + }, + }))} + /> +
+ + Total + + USD 10.30 + + <> + + + Earnings are generated when a referred user makes a + transaction on the platform. 20% of the fees from each + transaction are shared with the referrer. + + } + {...popoverProps} + /> + +
+ + ) : ( +
+ + You currently have no referrals. + + + Refer your friends to earn rewards. + +
+ )} +
+ ); +}; + +export default ReferralsTab; diff --git a/packages/commonwealth/client/scripts/views/components/Profile/ReferralsTab/index.ts b/packages/commonwealth/client/scripts/views/components/Profile/ReferralsTab/index.ts new file mode 100644 index 00000000000..e3a7c26971d --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/Profile/ReferralsTab/index.ts @@ -0,0 +1,3 @@ +import ReferralsTab from './ReferralsTab'; + +export default ReferralsTab; diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/CommunityMembersPage.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/CommunityMembersPage.tsx index 4521f84b51e..dafbe3aa9ea 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/CommunityMembersPage.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/CommunityMembersPage.tsx @@ -1,4 +1,5 @@ import { DEFAULT_NAME } from '@hicommonwealth/shared'; +import { OpenFeature } from '@openfeature/web-sdk'; import { APIOrderDirection } from 'helpers/constants'; import { useBrowserAnalyticsTrack } from 'hooks/useBrowserAnalyticsTrack'; import useTopicGating from 'hooks/useTopicGating'; @@ -36,6 +37,7 @@ import { CWTextInput } from 'views/components/component_kit/new_designs/CWTextIn import useAppStatus from '../../../../hooks/useAppStatus'; import './CommunityMembersPage.scss'; import GroupsSection from './GroupsSection'; +import LeaderboardSection from './LeaderboardSection'; import MembersSection from './MembersSection'; import { Member } from './MembersSection/MembersSection'; import { @@ -44,9 +46,21 @@ import { SearchFilters, } from './index.types'; +const client = OpenFeature.getClient(); +const referralsEnabled = client.getBooleanValue('referrals', false); + +enum TabValues { + AllMembers = 'all-members', + Leaderboard = 'leaderboard', + Groups = 'groups', +} + const TABS = [ - { value: 'all-members', label: 'All members' }, - { value: 'groups', label: 'Groups' }, + { value: TabValues.AllMembers, label: 'All members' }, + ...(referralsEnabled + ? [{ value: TabValues.Leaderboard, label: 'Leaderboard' }] + : []), + { value: TabValues.Groups, label: 'Groups' }, ]; const GROUP_AND_MEMBER_FILTERS: { label: string; value: BaseGroupFilter }[] = [ @@ -59,7 +73,9 @@ const CommunityMembersPage = () => { const navigate = useCommonNavigate(); const user = useUserStore(); - const [selectedTab, setSelectedTab] = useState(TABS[0].value); + const [selectedTab, setSelectedTab] = useState( + TabValues.AllMembers, + ); const [searchFilters, setSearchFilters] = useState({ searchText: '', groupFilter: GROUP_AND_MEMBER_FILTERS[0].value, @@ -270,16 +286,18 @@ const CommunityMembersPage = () => { const totalResults = members?.pages?.[0]?.totalResults || 0; - const updateActiveTab = (activeTab: string) => { + const updateActiveTab = (activeTab: TabValues) => { const params = new URLSearchParams(); params.set('tab', activeTab); navigate(`${window.location.pathname}?${params.toString()}`, {}, null); setSelectedTab(activeTab); let eventType; - if (activeTab === TABS[0].value) { + if (activeTab === TabValues.AllMembers) { eventType = MixpanelPageViewEvent.MEMBERS_PAGE_VIEW; - } else { + } else if (activeTab === TabValues.Leaderboard) { + eventType = MixpanelPageViewEvent.LEADERBOARD_PAGE_VIEW; + } else if (activeTab === TabValues.Groups) { eventType = MixpanelPageViewEvent.GROUPS_PAGE_VIEW; } @@ -298,14 +316,12 @@ const CommunityMembersPage = () => { useEffect(() => { // Set the active tab based on URL const params = new URLSearchParams(window.location.search.toLowerCase()); - const activeTab = params.get('tab')?.toLowerCase(); + const activeTab = + TABS.find((t) => t.value === params.get('tab')?.toLowerCase())?.value || + TabValues.AllMembers; - if (!activeTab || activeTab === TABS[0].value) { - updateActiveTab(TABS[0].value); - return; - } + updateActiveTab(activeTab); - updateActiveTab(TABS[1].value); // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.search]); @@ -348,7 +364,7 @@ const CommunityMembersPage = () => { {/* Gating group post-mutation banner */} {shouldShowGroupMutationBannerForCommunities.includes(communityId) && - selectedTab === TABS[0].value && ( + selectedTab === TabValues.AllMembers && (
{ )} {/* Filter section */} - {selectedTab === TABS[1].value && groups?.length === 0 ? ( + {selectedTab === TabValues.Leaderboard && groups?.length === 0 ? ( <> ) : (
{ size="large" fullWidth placeholder={`Search ${ - selectedTab === TABS[0].value ? 'members' : 'groups' + selectedTab === TabValues.AllMembers ? 'members' : 'groups' }`} containerClassName="search-input-container" inputClassName="search-input" @@ -435,12 +451,14 @@ const CommunityMembersPage = () => { )} {/* Main content section: based on the selected tab */} - {selectedTab === TABS[1].value ? ( + {selectedTab === TabValues.Groups ? ( + ) : selectedTab === TabValues.Leaderboard ? ( + ) : ( diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/LeaderboardSection/LeaderboardSection.scss b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/LeaderboardSection/LeaderboardSection.scss new file mode 100644 index 00000000000..dfa2636f437 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/LeaderboardSection/LeaderboardSection.scss @@ -0,0 +1,29 @@ +@import '../../../../../styles/shared'; + +.LeaderboardSection { + display: flex; + flex-direction: column; + gap: 24px; + + .search-input-container { + margin-block: 4px; + .text-input-left-icon { + position: absolute; + } + } + + .search-input { + padding-left: 48px !important; + } + + .search-icon { + position: relative; + left: 16px !important; + color: $neutral-400; + } + + table { + width: 100%; + @include table-cell; + } +} diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/LeaderboardSection/LeaderboardSection.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/LeaderboardSection/LeaderboardSection.tsx new file mode 100644 index 00000000000..16f628807e3 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/LeaderboardSection/LeaderboardSection.tsx @@ -0,0 +1,178 @@ +import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; + +import { APIOrderDirection } from 'helpers/constants'; +import { Avatar } from 'views/components/Avatar'; +import { CWIcon } from 'views/components/component_kit/cw_icons/cw_icon'; +import { CWTable } from 'views/components/component_kit/new_designs/CWTable'; +import { CWTableColumnInfo } from 'views/components/component_kit/new_designs/CWTable/CWTable'; +import { useCWTableState } from 'views/components/component_kit/new_designs/CWTable/useCWTableState'; +import { CWTextInput } from 'views/components/component_kit/new_designs/CWTextInput'; + +import './LeaderboardSection.scss'; + +const fakeData = [ + { + rank: 1, + user: { + name: 'cambell', + avatarUrl: + 'https://assets.commonwealth.im/794bb7a3-17d7-407a-b52e-2987501221b5.png', + userId: '128606', + address: 'address1', + }, + referrals: 30, + earnings: '0.0003', + referredBy: { + name: 'adam', + avatarUrl: + 'https://assets.commonwealth.im/0847e7f5-4d96-4406-8f30-c3082fa2f27c.png', + userId: '135099', + address: 'address2', + }, + }, + { + rank: 2, + user: { + name: 'adam', + avatarUrl: + 'https://assets.commonwealth.im/0847e7f5-4d96-4406-8f30-c3082fa2f27c.png', + userId: '135099', + address: 'address2', + }, + referrals: 20, + earnings: '0.0002', + referredBy: { + name: 'cambell', + avatarUrl: + 'https://assets.commonwealth.im/794bb7a3-17d7-407a-b52e-2987501221b5.png', + userId: '128606', + address: 'address1', + }, + }, + { + rank: 3, + user: { + name: 'mike', + avatarUrl: + 'https://assets.commonwealth.im/181e25ad-ce08-427d-8d3a-d290af3be44b.png', + userId: '158139', + address: 'address3', + }, + referrals: 10, + earnings: '0.0001', + referredBy: {}, + }, +]; + +const columns: CWTableColumnInfo[] = [ + { + key: 'rank', + header: 'Rank', + numeric: true, + sortable: true, + }, + { + key: 'member', + header: 'Member', + numeric: false, + sortable: true, + }, + { + key: 'referrals', + header: 'Referrals', + numeric: true, + sortable: true, + }, + { + key: 'earnings', + header: 'Earnings', + numeric: false, + sortable: true, + }, + { + key: 'referredBy', + header: 'Referred By', + numeric: false, + sortable: false, + }, +]; + +const LeaderboardSection = () => { + const [searchText, setSearchText] = useState(''); + + const tableState = useCWTableState({ + columns, + initialSortColumn: 'rank', + initialSortDirection: APIOrderDirection.Asc, + }); + + const filteredData = fakeData.filter((item) => + item.user.name.toLowerCase().includes(searchText.toLowerCase()), + ); + + return ( +
+ } + onInput={(e) => setSearchText(e.target.value?.trim())} + /> + ({ + ...item, + member: { + sortValue: item.user.name.toLowerCase(), + customElement: ( +
+ + +

{item.user.name}

+ +
+ ), + }, + earnings: { + sortValue: item.earnings, + customElement: ( +
ETH {item.earnings}
+ ), + }, + referredBy: { + customElement: ( +
+ + +

{item?.referredBy?.name}

+ +
+ ), + }, + }))} + /> +
+ ); +}; + +export default LeaderboardSection; diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/LeaderboardSection/index.ts b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/LeaderboardSection/index.ts new file mode 100644 index 00000000000..cff44855126 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/LeaderboardSection/index.ts @@ -0,0 +1,3 @@ +import LeaderboardSection from './LeaderboardSection'; + +export default LeaderboardSection; diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/MembersSection/MembersSection.scss b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/MembersSection/MembersSection.scss index 73496db9536..cba053373cf 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/MembersSection/MembersSection.scss +++ b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Members/MembersSection/MembersSection.scss @@ -10,37 +10,6 @@ table { width: 100%; - - .table-cell { - display: flex; - gap: 8px; - align-items: center; - flex-wrap: wrap; - - .user-info { - text-decoration: none; - display: flex; - align-items: center; - gap: 8px; - color: black; - - div { - display: flex; - justify-content: center; - align-items: center; - } - - :hover, - :focus, - :visited { - text-decoration: underline; - } - } - - &.text-right { - display: block; - text-align: right; - } - } + @include table-cell; } } diff --git a/packages/commonwealth/shared/analytics/types.ts b/packages/commonwealth/shared/analytics/types.ts index f2ac69fb7da..81ed7e3ab7e 100644 --- a/packages/commonwealth/shared/analytics/types.ts +++ b/packages/commonwealth/shared/analytics/types.ts @@ -8,6 +8,7 @@ export const enum MixpanelPageViewEvent { GROUPS_CREATION_PAGE_VIEW = 'Create Group Page Viewed', GROUPS_EDIT_PAGE_VIEW = 'Edit Group Page Viewed', DIRECTORY_PAGE_VIEW = 'Directory Page Viewed', + LEADERBOARD_PAGE_VIEW = 'Leaderboard Page Viewed', } export const enum MixpanelCommunityInteractionEvent {