diff --git a/package.json b/package.json index c69df0ba8..894985bf6 100644 --- a/package.json +++ b/package.json @@ -92,8 +92,8 @@ "@subsocial/api": "0.8.10", "@subsocial/definitions": "0.8.10", "@subsocial/elasticsearch": "0.8.10", - "@subsocial/grill-widget": "^0.0.12", - "@subsocial/resource-discussions": "^0.0.3", + "@subsocial/grill-widget": "^0.0.13", + "@subsocial/resource-discussions": "^0.0.4", "@subsocial/utils": "0.8.10", "@tiptap/extension-highlight": "^2.0.0-beta.33", "@tiptap/extension-image": "^2.0.0-beta.27", @@ -107,6 +107,7 @@ "@tiptap/pm": "^2.0.3", "@tiptap/react": "^v2.0.0-beta.217", "@tiptap/starter-kit": "^2.0.0-beta.184", + "@types/lodash.shuffle": "^4.2.9", "adblock-detect-react": "^1.0.5", "ant-design-pro": "^2.3.2", "antd": "4.12.3", @@ -128,6 +129,7 @@ "lodash.isempty": "^4.4.0", "lodash.memoize": "^4.1.2", "lodash.partition": "^4.6.0", + "lodash.shuffle": "^4.2.0", "lodash.truncate": "^4.4.2", "next": "11.1.4", "next-themes": "^0.0.14", @@ -154,6 +156,7 @@ "react-tooltip": "^4.2.19", "sanitize-html": "^2.10.0", "sass": "^1.26.10", + "sort-keys-recursive": "^2.1.10", "store": "^2.0.12", "strip-markdown": "^4.0.0", "url-loader": "^4.1.1", diff --git a/public/images/creators/active-staking.jpeg b/public/images/creators/active-staking.jpeg new file mode 100644 index 000000000..2ec6abdb7 Binary files /dev/null and b/public/images/creators/active-staking.jpeg differ diff --git a/public/images/creators/hearts.png b/public/images/creators/hearts.png new file mode 100644 index 000000000..372c09b56 Binary files /dev/null and b/public/images/creators/hearts.png differ diff --git a/public/images/creators/registered-creators.jpeg b/public/images/creators/registered-creators.jpeg new file mode 100644 index 000000000..5c5628a73 Binary files /dev/null and b/public/images/creators/registered-creators.jpeg differ diff --git a/public/images/creators/subsocial-tokens.png b/public/images/creators/subsocial-tokens.png new file mode 100644 index 000000000..753a019c7 Binary files /dev/null and b/public/images/creators/subsocial-tokens.png differ diff --git a/public/images/databases.svg b/public/images/databases.svg new file mode 100644 index 000000000..7ecbe5bfb --- /dev/null +++ b/public/images/databases.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/chat/ChatFloatingModal.module.sass b/src/components/chat/ChatFloatingModal.module.sass index 572ca194f..936b134bf 100644 --- a/src/components/chat/ChatFloatingModal.module.sass +++ b/src/components/chat/ChatFloatingModal.module.sass @@ -110,3 +110,55 @@ opacity: 0 transform: translateY(100%) + +.Position--right + .ChatContainer + align-items: flex-end + + .ChatContent + max-width: 570px + width: 100% + position: relative + height: 100vh + height: 100dvh + + .ChatControl + padding: 0 + position: absolute + left: -40px + top: 12px + transform: rotate(-90deg) + + &.ChatContainerHidden + .ChatContent + transform: translateX(100%) + +.Position--bottom + .ChatContainer + &.ChatContainerHidden + .ChatContent + transform: translateY(100%) + +@media ( max-width: $max_mobile_width ) + .Position--right + .ChatContainer + align-items: center + + .ChatContent + max-width: none + width: 100% + position: relative + height: 90vh + height: 90dvh + + .ChatControl + padding: $space-tiny + top: 0 + left: 0 + position: relative + transform: rotate(0deg) + + &.ChatContainerHidden + .ChatContent + transform: translateY(100%) + diff --git a/src/components/chat/ChatFloatingModal.tsx b/src/components/chat/ChatFloatingModal.tsx index b0959c24b..ef92bc005 100644 --- a/src/components/chat/ChatFloatingModal.tsx +++ b/src/components/chat/ChatFloatingModal.tsx @@ -1,6 +1,6 @@ import { Button } from 'antd' import clsx from 'clsx' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useState } from 'react' import { createPortal } from 'react-dom' import { HiChevronDown } from 'react-icons/hi2' import { useSendEvent } from 'src/providers/AnalyticContext' @@ -8,15 +8,14 @@ import { useChatOpenState } from 'src/rtk/app/hooks' import { useAppSelector } from 'src/rtk/app/store' import { ChatEntity } from 'src/rtk/features/chat/chatSlice' import { disablePageScroll, enablePageScroll } from 'src/utils/window' -import { useResponsiveSize } from '../responsive' import styles from './ChatFloatingModal.module.sass' import ChatIframe from './ChatIframe' export default function ChatFloatingModal() { - const { isLargeDesktop } = useResponsiveSize() const sendEvent = useSendEvent() const [isOpen, setIsOpen] = useChatOpenState() const entity = useAppSelector(state => state.chat.entity) + const withFloatingButton = useAppSelector(state => state.chat.withFloatingButton) const [unreadCount, setUnreadCount] = useState(0) @@ -29,27 +28,26 @@ export default function ChatFloatingModal() { } }, [entity]) - const hasOpened = useRef(false) + useEffect(() => { + if (isOpen) disablePageScroll() + else enablePageScroll() + + if (entity && isOpen) { + setUnreadCount(0) + saveUnreadCount(entity, 0) + } + }, [isOpen]) + const toggleChat = () => { let event if (isOpen) { event = 'close_grill_iframe' } else { event = 'open_grill_iframe' - setUnreadCount(0) - if (entity) saveUnreadCount(entity, 0) } sendEvent(event) - if (!isOpen) disablePageScroll() - else enablePageScroll() - setIsOpen(!isOpen) - hasOpened.current = true - } - - if (isLargeDesktop) { - return null } const onUnreadCountChange = (count: number) => { @@ -64,29 +62,32 @@ export default function ChatFloatingModal() { return ( <> {createPortal( -
-
setIsOpen(false)} /> -
-
- +
+
+
setIsOpen(false)} /> +
+
+ +
+
-
, document.body, )} - {createPortal( -
- - {!!unreadCount && {unreadCount}} -
, - document.body, - )} + {withFloatingButton && + createPortal( +
+ + {!!unreadCount && {unreadCount}} +
, + document.body, + )} ) } diff --git a/src/components/chat/ChatIframe.module.sass b/src/components/chat/ChatIframe.module.sass new file mode 100644 index 000000000..fbe569dc4 --- /dev/null +++ b/src/components/chat/ChatIframe.module.sass @@ -0,0 +1,6 @@ +.ChatIframe + transition: opacity 150ms ease-out + opacity: 1 + + &.ChatIframeLoading + opacity: 0 \ No newline at end of file diff --git a/src/components/chat/ChatIframe.tsx b/src/components/chat/ChatIframe.tsx index 6c087d68e..8bc460c62 100644 --- a/src/components/chat/ChatIframe.tsx +++ b/src/components/chat/ChatIframe.tsx @@ -2,13 +2,15 @@ import grill, { GrillConfig, GrillEventListener } from '@subsocial/grill-widget' import { Resource } from '@subsocial/resource-discussions' import { summarizeMd } from '@subsocial/utils' import clsx from 'clsx' -import { ComponentProps, useEffect } from 'react' +import { ComponentProps, useEffect, useState } from 'react' import config from 'src/config' import useWrapInRef from 'src/hooks/useWrapInRef' import { useSendEvent } from 'src/providers/AnalyticContext' import { useSetChatTotalMessageCount } from 'src/rtk/app/hooks' import { useAppSelector } from 'src/rtk/app/store' import { ChatEntity } from 'src/rtk/features/chat/chatSlice' +import { getCurrentWallet } from '../auth/utils' +import styles from './ChatIframe.module.sass' export type ChatIframeProps = ComponentProps<'div'> & { onUnreadCountChange?: (count: number) => void @@ -18,6 +20,7 @@ export default function ChatIframe({ onUnreadCountChange, ...props }: ChatIframe const entity = useAppSelector(state => state.chat.entity) const sendEvent = useSendEvent() const sendEventRef = useWrapInRef(sendEvent) + const [isLoading, setIsLoading] = useState(false) const setChatTotalMessageCount = useSetChatTotalMessageCount() useEffect(() => { @@ -25,6 +28,10 @@ export default function ChatIframe({ onUnreadCountChange, ...props }: ChatIframe const config = generateGrillConfig(entity) if (!config) return config.onWidgetCreated = iframe => { + const currentWallet = getCurrentWallet() + if (currentWallet) { + iframe.src = `${iframe.src}&wallet=${currentWallet}` + } iframe.onerror = () => { sendEventRef.current('chat_widget_error') } @@ -38,57 +45,126 @@ export default function ChatIframe({ onUnreadCountChange, ...props }: ChatIframe const parsedValue = parseInt(value) ?? 0 if (name === 'unread') onUnreadCountChange?.(parsedValue) else if (name === 'totalMessage') setChatTotalMessageCount(parsedValue) + else if (name === 'isUpdatingConfig') { + if (value === 'true') { + setIsLoading(true) + } else if (value === 'false') { + setIsLoading(false) + } + } } if (listener) { - grill.addUnreadCountListener(listener) + grill.addMessageListener(listener) } - grill.init(config) + if (document.contains(grill.instances?.['grill']?.iframe)) { + grill.setConfig(config) + } else { + grill.init(config) + } return () => { - if (listener) grill.removeUnreadCountListener(listener) + if (listener) grill.removeMessageListener(listener) } }, [entity, sendEventRef]) - return
+ return ( +
+ ) +} + +type CommonSettings = { + settings: NonNullable['settings'] + root: Partial } function generateGrillConfig(entity: ChatEntity['entity']): GrillConfig | null { if (!entity) return null - if (entity.type === 'post') { - const post = entity.data - const title = summarizeMd(post.content?.title || post.content?.body || '', { - limit: 50, - }).summary - const body = summarizeMd(post.content?.body ?? '', { limit: 50 }).summary - return { - hub: { - id: config.commentsHubId, - }, + const commonSettings: CommonSettings = { + root: { theme: 'light', rootFontSize: '1rem', - channel: { - type: 'resource', - resource: new Resource({ - schema: 'social', - app: 'polkaverse', - resourceType: 'post', - resourceValue: { - id: post.struct.id, - }, - }), - settings: { - enableLoginButton: true, - enableInputAutofocus: true, + }, + settings: { + enableLoginButton: true, + enableInputAutofocus: true, + }, + } + if (entity.type === 'post') { + return generatePostGrillConfig(entity, commonSettings) + } else if (entity.type === 'space') { + return generateSpaceGrillConfig(entity, commonSettings) + } + + return null +} + +const creatorsHubId = '1218' +function generateSpaceGrillConfig( + entity: Extract, + commonSettings: CommonSettings, +): GrillConfig { + const space = entity.data + const { content } = space + const metadata = { + title: content?.name ?? '', + body: content?.about ?? '', + image: content?.image ?? '', + } + + return { + ...commonSettings.root, + hub: { id: creatorsHubId }, + channel: { + ...commonSettings.settings, + type: 'resource', + resource: new Resource({ + schema: 'chain', + chainType: 'substrate', + chainName: 'subsocial', + resourceType: 'creator', + resourceValue: { + id: space.id, }, - metadata: { - title, - body, - image: post.content?.image, + }), + metadata, + }, + } +} + +function generatePostGrillConfig( + entity: Extract, + commonSettings: CommonSettings, +): GrillConfig { + const post = entity.data + const title = summarizeMd(post.content?.title || post.content?.body || '', { + limit: 50, + }).summary + const body = summarizeMd(post.content?.body ?? '', { limit: 50 }).summary + + return { + ...commonSettings.root, + hub: { id: config.commentsHubId }, + channel: { + ...commonSettings.settings, + type: 'resource', + resource: new Resource({ + schema: 'social', + app: 'polkaverse', + resourceType: 'post', + resourceValue: { + id: post.struct.id, }, + }), + metadata: { + title, + body, + image: post.content?.image, }, - } + }, } - - return null } diff --git a/src/components/comments/CommentsSection.tsx b/src/components/comments/CommentsSection.tsx index a014f4541..572bf6c4a 100644 --- a/src/components/comments/CommentsSection.tsx +++ b/src/components/comments/CommentsSection.tsx @@ -74,7 +74,7 @@ export const CommentPage: NextPage = ({ comment, parentPost, s } return ( - + {renderResponseTitle()} diff --git a/src/components/creators/CreatorDashboardSidebar.tsx b/src/components/creators/CreatorDashboardSidebar.tsx new file mode 100644 index 000000000..556208544 --- /dev/null +++ b/src/components/creators/CreatorDashboardSidebar.tsx @@ -0,0 +1,104 @@ +import { SpaceData } from '@subsocial/api/types' +import clsx from 'clsx' +import { ComponentProps } from 'react' +import { useIsCreatorSpace } from 'src/rtk/features/creators/creatorsListHooks' +import { useFetchStakeData } from 'src/rtk/features/creators/stakesHooks' +import { useMyAddress } from '../auth/MyAccountsContext' +import CreatePostCard from './cards/CreatePostCard' +import CreatorInfoCard from './cards/CreatorInfoCard' +import GetMoreSubCard from './cards/GetMoreSubCard' +import MyStakeCard from './cards/MyStakeCard' +import StakeSubCard from './cards/StakeSubCard' +import SupportCreatorsCard from './cards/SupportCreatorsCard' + +export type CreatorDashboardHomeVariant = 'posts' | 'spaces' +export type CreatorDashboardSidebarType = + | { name: 'home-page'; variant: CreatorDashboardHomeVariant } + | { name: 'space-page'; space: SpaceData } + | { name: 'post-page'; space: SpaceData } + +export type CreatorDashboardSidebarProps = ComponentProps<'div'> & { + dashboardType: CreatorDashboardSidebarType +} + +export default function CreatorDashboardSidebar({ + dashboardType, + ...props +}: CreatorDashboardSidebarProps) { + if (!dashboardType) return null + + let content: JSX.Element | null = null + if (dashboardType.name === 'home-page') { + content = + } else if (dashboardType.name === 'space-page') { + content = + } else if (dashboardType.name === 'post-page') { + content = + } + + return ( +
+ {content} +
+ ) +} + +function HomePageSidebar({ variant }: Extract) { + return ( + <> + + + + ) +} + +function SpacePageSidebar({ space }: Extract) { + const myAddress = useMyAddress() + const { data } = useFetchStakeData(myAddress ?? '', space.id) + const { isCreatorSpace, loading } = useIsCreatorSpace(space.id) + + if (loading) { + return null + } + + if (!isCreatorSpace) { + return + } + + return data?.hasStaked ? ( + <> + + + + ) : ( + <> + + + ) +} + +function PostPageSidebar({ space }: Extract) { + const myAddress = useMyAddress() + const { data, loading } = useFetchStakeData(myAddress ?? '', space.id) + const { isCreatorSpace, loading: loadingCreator } = useIsCreatorSpace(space.id) + + if (loadingCreator) { + return null + } + + if (!isCreatorSpace) { + return ( + <> + + + + ) + } + + return ( + <> + + {loading ? null : data?.hasStaked ? : } + + ) +} diff --git a/src/components/creators/MobileIncreaseSubRewards.module.sass b/src/components/creators/MobileIncreaseSubRewards.module.sass new file mode 100644 index 000000000..4718444a8 --- /dev/null +++ b/src/components/creators/MobileIncreaseSubRewards.module.sass @@ -0,0 +1,26 @@ +@import 'src/styles/subsocial-vars.scss' + +.MobileIncreaseSubRewards + display: flex + position: relative + align-items: center + justify-content: space-between + gap: $space_normal + background: #FFEDF5 + border-radius: 0px 0px $border_radius_huge $border_radius_huge + padding: $space_normal + overflow: hidden + + .Title, .Link + font-weight: $font_weight_semibold + + .Gradient + position: absolute + background: rgba(132, 78, 247, 0.18) + top: 50% + transform: translateY(-50%) + filter: blur(130px) + left: 75% + border-radius: 50% + width: 400px + height: 400px \ No newline at end of file diff --git a/src/components/creators/MobileIncreaseSubRewards.tsx b/src/components/creators/MobileIncreaseSubRewards.tsx new file mode 100644 index 000000000..aa93e6e41 --- /dev/null +++ b/src/components/creators/MobileIncreaseSubRewards.tsx @@ -0,0 +1,30 @@ +import { SpaceData } from '@subsocial/api/types' +import clsx from 'clsx' +import Link from 'next/link' +import { ComponentProps } from 'react' +import { useFetchStakeData } from 'src/rtk/features/creators/stakesHooks' +import { activeStakingLinks } from 'src/utils/links' +import { useMyAddress } from '../auth/MyAccountsContext' +import styles from './MobileIncreaseSubRewards.module.sass' + +export type MobileIncreaseSubRewardsProps = ComponentProps<'div'> & { + space: SpaceData +} + +export default function MobileIncreaseSubRewards(props: MobileIncreaseSubRewardsProps) { + const myAddress = useMyAddress() + const { data } = useFetchStakeData(myAddress ?? '', props.space.id) + if (!data?.hasStaked) return null + + return ( +
+ Increase SUB rewards + + + Learn more + + +
+
+ ) +} diff --git a/src/components/creators/cards/CreatePostCard.module.sass b/src/components/creators/cards/CreatePostCard.module.sass new file mode 100644 index 000000000..04b39c888 --- /dev/null +++ b/src/components/creators/cards/CreatePostCard.module.sass @@ -0,0 +1,24 @@ +@import 'src/styles/subsocial-vars.scss' + +.CreatePostCard + border-radius: $border_radius_big + padding: $space_normal + display: flex + flex-direction: column + + .TitleContainer + display: flex + align-items: center + gap: $space_small + margin-bottom: $space_small + + .Image + width: 44px + height: 44px + border-radius: 100% + flex-shrink: 0 + + .Title + font-size: $font_large + font-weight: $font_weight_semibold + line-height: normal diff --git a/src/components/creators/cards/CreatePostCard.tsx b/src/components/creators/cards/CreatePostCard.tsx new file mode 100644 index 000000000..b77c07b67 --- /dev/null +++ b/src/components/creators/cards/CreatePostCard.tsx @@ -0,0 +1,72 @@ +import { Button, Skeleton } from 'antd' +import clsx from 'clsx' +import Link from 'next/link' +import { HiArrowUpRight } from 'react-icons/hi2' +import { useMyAddress } from 'src/components/auth/MyAccountsContext' +import { CreatePostButtonAndModal } from 'src/components/posts/NewPostButtonInTopMenu' +import { CreateSpaceButton } from 'src/components/spaces/helpers' +import { DfImage } from 'src/components/utils/DfImage' +import Segment from 'src/components/utils/Segment' +import { useSelectSpaceIdsWhereAccountCanPostWithLoadingStatus } from 'src/rtk/app/hooks' +import { selectSpaceIdsThatCanSuggestIfSudo } from 'src/utils' +import { activeStakingLinks } from 'src/utils/links' +import { CreatorDashboardHomeVariant } from '../CreatorDashboardSidebar' +import styles from './CreatePostCard.module.sass' + +export type CreatePostCardProps = { + variant: CreatorDashboardHomeVariant +} + +export default function CreatePostCard({ variant }: CreatePostCardProps) { + const myAddress = useMyAddress() + + const { isLoading, spaceIds: ids } = + useSelectSpaceIdsWhereAccountCanPostWithLoadingStatus(myAddress) + const spaceIds = selectSpaceIdsThatCanSuggestIfSudo({ myAddress, spaceIds: ids }) + + const anySpace = spaceIds[0] + + let imagePath = '/images/creators/active-staking.jpeg' + if (variant === 'spaces') imagePath = '/images/creators/registered-creators.jpeg' + + return ( + +
+ + + {variant === 'posts' ? Active Staking : Featured Creators} + +
+ + By creating new posts and liking new content of others, stakers of SUB can increase their + staking rewards by 50% to 200%.{' '} + + + Learn more{' '} + + + + +
+ {isLoading ? ( + + ) : anySpace ? ( + + {onClick => ( + + )} + + ) : ( +
+ Create a profile to get started. + + Create profile + +
+ )} +
+
+ ) +} diff --git a/src/components/creators/cards/CreatorInfoCard.module.sass b/src/components/creators/cards/CreatorInfoCard.module.sass new file mode 100644 index 000000000..2b558830c --- /dev/null +++ b/src/components/creators/cards/CreatorInfoCard.module.sass @@ -0,0 +1,23 @@ +@import 'src/styles/subsocial-vars.scss' + +.CreatorInfoCard + border-radius: $border_radius_big + padding: $space_normal + display: flex + flex-direction: column + + .TitleContainer + display: flex + align-items: center + gap: $space_small + margin-bottom: $space_small + + .Title + font-size: $font_large + font-weight: $font_weight_semibold + line-height: normal + margin-bottom: $space_mini + + .Subtitle + opacity: 0.8 + font-size: $font_small diff --git a/src/components/creators/cards/CreatorInfoCard.tsx b/src/components/creators/cards/CreatorInfoCard.tsx new file mode 100644 index 000000000..fdd0e283e --- /dev/null +++ b/src/components/creators/cards/CreatorInfoCard.tsx @@ -0,0 +1,56 @@ +import { SpaceData } from '@subsocial/api/types' +import { Button } from 'antd' +import clsx from 'clsx' +import { useMyAddress } from 'src/components/auth/MyAccountsContext' +import { SpaceFollowersModal } from 'src/components/profiles/AccountsListModal' +import { OfficialSpaceStatus, SpaceAvatar } from 'src/components/spaces/helpers' +import CollapsibleParagraph from 'src/components/utils/CollapsibleParagraph/CollapsibleParagraph' +import FollowSpaceButton from 'src/components/utils/FollowSpaceButton' +import { Pluralize } from 'src/components/utils/Plularize' +import Segment from 'src/components/utils/Segment' +import { useFetchStakeData } from 'src/rtk/features/creators/stakesHooks' +import { getSubIdCreatorsLink } from 'src/utils/links' +import styles from './CreatorInfoCard.module.sass' + +export type CreatorInfoCardProps = { + space: SpaceData +} + +export default function CreatorInfoCard({ space }: CreatorInfoCardProps) { + const myAddress = useMyAddress() + const { data } = useFetchStakeData(myAddress ?? '', space.id) + + return ( + +
+ +
+ + {space.content?.name ?? 'Untitled'} + + + + !!count && ( +
+ +
+ ) + } + /> +
+
+ +
+ {data?.hasStaked && ( + + )} + +
+
+ ) +} diff --git a/src/components/creators/cards/GetMoreSubCard.module.sass b/src/components/creators/cards/GetMoreSubCard.module.sass new file mode 100644 index 000000000..c2fd46924 --- /dev/null +++ b/src/components/creators/cards/GetMoreSubCard.module.sass @@ -0,0 +1,58 @@ +@import 'src/styles/subsocial-vars.scss' + +.GetMoreSub + background: #A81580 + border-radius: $border_radius_big + color: white + overflow: clip + position: relative + + .Content + z-index: 1 + position: relative + + .Title + font-size: $font_large + font-weight: $font_weight_semibold + line-height: normal + + .Subtitle + font-size: $font_small + opacity: 0.8 + + .Image + float: right + width: 142px + margin-right: -64px + margin-top: -36px + * + width: 100% + + .OutlineButton + color: white + border-color: white + + &:hover, &:focus-within + color: #e3cadd + border-color: #e3cadd + + .Gradient + width: 500px + height: 500px + position: absolute + background: #EF35C6 + top: 0 + left: 50% + filter: blur(310px) + border-radius: 100% + + .Gradient2 + width: 500px + height: 500px + position: absolute + background: rgba(249, 121, 221, 0.69) + top: 50% + left: 0% + filter: blur(100px) + border-radius: 100% + diff --git a/src/components/creators/cards/GetMoreSubCard.tsx b/src/components/creators/cards/GetMoreSubCard.tsx new file mode 100644 index 000000000..bf892d350 --- /dev/null +++ b/src/components/creators/cards/GetMoreSubCard.tsx @@ -0,0 +1,49 @@ +import { Button } from 'antd' +import clsx from 'clsx' +import { ComponentProps } from 'react' +import { DfImage } from 'src/components/utils/DfImage' +import Segment from 'src/components/utils/Segment' +import { activeStakingLinks } from 'src/utils/links' +import styles from './GetMoreSubCard.module.sass' + +export type GetMoreSubCardProps = ComponentProps<'div'> + +export default function GetMoreSubCard({ ...props }: GetMoreSubCardProps) { + return ( + +
+ +

Get more SUB with Active Staking

+

Get rewarded based on your social activity

+
+ + +
+
+
+
+ + ) +} diff --git a/src/components/creators/cards/MyStakeCard.module.sass b/src/components/creators/cards/MyStakeCard.module.sass new file mode 100644 index 000000000..e1ab89fdd --- /dev/null +++ b/src/components/creators/cards/MyStakeCard.module.sass @@ -0,0 +1,49 @@ +@import 'src/styles/subsocial-vars.scss' + +.CreatorStakingCard + display: flex + flex-direction: column + padding: 0 + border-radius: $border_radius_big + + .TopSection + padding: $space_normal + border-bottom: 1px solid #E2E8F0 + position: relative + overflow: hidden + + .Title + font-size: $font_large + font-weight: $font_weight_semibold + + .Link + font-size: $font_small + + .Image + position: absolute + top: -$space_mini + right: -$space_mini + height: 100% + + * + height: 100% + object-fit: contain + + .BottomSection + padding: $space_normal + + .MyStake + display: flex + justify-content: space-between + + .HelpIcon + position: relative + top: 1px + +.Skeleton + display: flex + align-items: center + width: 100px + * + margin: 0 !important + display: block \ No newline at end of file diff --git a/src/components/creators/cards/MyStakeCard.tsx b/src/components/creators/cards/MyStakeCard.tsx new file mode 100644 index 000000000..50ff10089 --- /dev/null +++ b/src/components/creators/cards/MyStakeCard.tsx @@ -0,0 +1,74 @@ +import { SpaceData } from '@subsocial/api/types' +import { Button, Skeleton } from 'antd' +import clsx from 'clsx' +import Link from 'next/link' +import { BsBoxArrowUpRight } from 'react-icons/bs' +import { SlQuestion } from 'react-icons/sl' +import { useMyAddress } from 'src/components/auth/MyAccountsContext' +import { FormatBalance } from 'src/components/common/balances' +import { useResponsiveSize } from 'src/components/responsive' +import { DfImage } from 'src/components/utils/DfImage' +import Segment from 'src/components/utils/Segment' +import { useFetchStakeData } from 'src/rtk/features/creators/stakesHooks' +import { getSubIdCreatorsLink } from 'src/utils/links' +import styles from './MyStakeCard.module.sass' + +export type MyStakeCardProps = { + space: SpaceData +} + +export default function MyStakeCard({ space }: MyStakeCardProps) { + const myAddress = useMyAddress() + const { data, loading } = useFetchStakeData(myAddress ?? '', space.id) + const { isMobile } = useResponsiveSize() + + return ( + + {!isMobile && ( +
+

Creator Staking

+ + + How does it work? + + + +
+ )} +
+
+
+ My Stake + +
+ {loading ? ( + + ) : ( + + + + )} +
+ +
+
+ ) +} diff --git a/src/components/creators/cards/StakeSubCard.module.sass b/src/components/creators/cards/StakeSubCard.module.sass new file mode 100644 index 000000000..642f2c8e7 --- /dev/null +++ b/src/components/creators/cards/StakeSubCard.module.sass @@ -0,0 +1,52 @@ +@import 'src/styles/subsocial-vars.scss' + +.StakeSubCard + display: flex + border: 1px solid #6366F1 + background: #EDF4FF + border-radius: $border_radius_big + padding: $space_normal + justify-content: space-between + overflow: hidden + + .Content + display: block + width: 100% + + .Title + font-size: $font_large + font-weight: $font_weight_medium + line-height: normal + + @media screen and (max-width: 455px) + & + font-size: $font_semilarge + + .Subtitle + font-size: $font_small + opacity: 0.8 + + .Image + display: block + float: right + width: 115px + position: relative + margin-top: -25px + margin-right: -56px + + :global(.ant-image), :global(.ant-image-img) + // width: 100% + height: 100% + object-fit: contain + + @media screen and (max-width: 767px) + & + width: 140px + margin-top: -10px + margin-right: -10px + + @media screen and (max-width: 455px) + & + width: 115px + margin-top: -10px + margin-right: -10px diff --git a/src/components/creators/cards/StakeSubCard.tsx b/src/components/creators/cards/StakeSubCard.tsx new file mode 100644 index 000000000..51500006c --- /dev/null +++ b/src/components/creators/cards/StakeSubCard.tsx @@ -0,0 +1,34 @@ +import { SpaceData } from '@subsocial/api/types' +import { Button } from 'antd' +import clsx from 'clsx' +import { useResponsiveSize } from 'src/components/responsive' +import { DfImage } from 'src/components/utils/DfImage' +import { getSubIdCreatorsLink } from 'src/utils/links' +import styles from './StakeSubCard.module.sass' + +export type StakeSubCardProps = { + space: SpaceData +} + +export default function StakeSubCard({ space }: StakeSubCardProps) { + const { isSmallMobile, isNotMobile } = useResponsiveSize() + return ( +
+
+ +

Stake SUB to this creator and earn more

+

+ Generate rewards for both you and this creator by staking towards them +

+ +
+
+ ) +} diff --git a/src/components/creators/cards/SupportCreatorsCard.module.sass b/src/components/creators/cards/SupportCreatorsCard.module.sass new file mode 100644 index 000000000..fb20cb890 --- /dev/null +++ b/src/components/creators/cards/SupportCreatorsCard.module.sass @@ -0,0 +1,59 @@ +@import 'src/styles/subsocial-vars.scss' + +.SupportCreatorsCard + padding: $space_normal + background-color: #2D81FF + border-radius: $border_radius_big + position: relative + z-index: 0 + overflow: clip + + .Content + color: white + z-index: 1 + position: relative + + .Title + line-height: normal + + .Subtitle + opacity: 0.8 + font-size: $font_small + + .Image + display: block + float: right + width: 115px + position: relative + margin-top: -25px + margin-right: -56px + + .Gradient1 + position: absolute + width: 1000px + height: 1000px + border-radius: 100% + left: 50% + top: 0 + background: #A897FA + filter: blur(300px) + + .Gradient1 + position: absolute + width: 1000px + height: 1000px + border-radius: 100% + left: 50% + top: 0 + background: #A897FA + filter: blur(300px) + + .Gradient2 + position: absolute + width: 300px + height: 300px + border-radius: 100% + top: 50% + left: 0 + background: rgba(198, 193, 252, 0.69) + filter: blur(135px) diff --git a/src/components/creators/cards/SupportCreatorsCard.tsx b/src/components/creators/cards/SupportCreatorsCard.tsx new file mode 100644 index 000000000..afd53504a --- /dev/null +++ b/src/components/creators/cards/SupportCreatorsCard.tsx @@ -0,0 +1,27 @@ +import { Button } from 'antd' +import clsx from 'clsx' +import { DfImage } from 'src/components/utils/DfImage' +import { getSubIdCreatorsLink } from 'src/utils/links' +import styles from './SupportCreatorsCard.module.sass' + +export default function SupportCreatorsCard() { + return ( +
+
+ +

+ Support creators and earn SUB +

+

+ Generate rewards for both you and this creator by staking towards them +

+ +
+ +
+
+
+ ) +} diff --git a/src/components/main/HomePage.tsx b/src/components/main/HomePage.tsx index d76fa94f3..551f0542d 100644 --- a/src/components/main/HomePage.tsx +++ b/src/components/main/HomePage.tsx @@ -10,7 +10,8 @@ import { GetHomePageData } from 'src/graphql/__generated__/GetHomePageData' import { getInitialPropsWithRedux } from 'src/rtk/app' import { PostKind } from 'src/types/graphql-global-types' import { useIsSignedIn } from '../auth/MyAccountsContext' -import GetSubBanner from '../utils/banners/GetSubBanner' +import { CreatorDashboardHomeVariant } from '../creators/CreatorDashboardSidebar' +import { CreatorsSpaces } from '../spaces/LatestSpacesPage' import Section from '../utils/Section' import style from './HomePage.module.sass' import { dateFilterOpt, Filters, PostFilterView, SpaceFilterView } from './HomePageFilters' @@ -60,7 +61,8 @@ const HomeTabs = (props: TabsProps) => { <> - + + @@ -80,7 +82,12 @@ const AffixTabs = (props: AffixTabsProps) => { const ToTopIcon = -const TabsHomePage = (props: Props) => { +const TabsHomePage = ({ + setCurrentTabVariant, + ...props +}: Props & { + setCurrentTabVariant: (variant: CreatorDashboardHomeVariant) => void +}) => { const isSignedIn = useIsSignedIn() const router = useRouter() let prevScrollpos = 0 @@ -110,6 +117,12 @@ const TabsHomePage = (props: Props) => { const type = getFilterType(tab, typeFromUrl) const date = dateFilterOpt[dateFilterIndex].value as DateFilterType + useEffect(() => { + let variant: CreatorDashboardHomeVariant = 'posts' + if (tab === 'spaces' || tab === 'creators') variant = 'spaces' + setCurrentTabVariant(variant) + }, [setCurrentTabVariant, tab]) + const onChangeKey = (key: string) => { const typeValue = getFilterType(key, type) const filterType = @@ -145,8 +158,10 @@ const TabsHomePage = (props: Props) => { {...props} /> ) - } else { + } else if (tab === 'spaces') { return + } else { + return } }, [tab, type, date]) @@ -168,19 +183,27 @@ const TabsHomePage = (props: Props) => { ) } -const HomePage: NextPage = props => ( - <> - - {/* */} - - - - -) +const HomePage: NextPage = props => { + const [currentTabVariant, setCurrentTabVariant] = useState('posts') + + return ( + <> + + {/* */} + {/* */} + + + + ) +} getInitialPropsWithRedux(HomePage, async ({ apolloClient }) => { const apolloRes = await apolloClient?.query({ diff --git a/src/components/main/HomePageFilters.tsx b/src/components/main/HomePageFilters.tsx index 4bc4f7a1b..60feab5a2 100644 --- a/src/components/main/HomePageFilters.tsx +++ b/src/components/main/HomePageFilters.tsx @@ -29,7 +29,7 @@ const offchainPostFilterOpt = enableGraphQl : [] export const postFilterOpt = [ - { label: 'Polkadot News', value: 'suggested' }, + { label: 'Recommended', value: 'suggested' }, ...offchainPostFilterOpt, ] @@ -65,6 +65,7 @@ export const filterByKey = { posts: postFilterOpt, comments: commentFilterOpt, spaces: spaceFilterOpt, + creators: [], } type OnChangeFn = (value: any) => void @@ -100,7 +101,9 @@ export const Filters = (props: Props) => { const onDateChange: any = (value: DateFilterType = 'week') => setFiltersInUrl(router, tabKey, { type: type as EntityFilter, date: value }) - const needDateFilter = type !== 'latest' && type !== 'suggested' + const needDateFilter = !!type && type !== 'latest' && type !== 'suggested' + + if (!needDateFilter && !filterByKey[tabKey]?.length) return null return (
diff --git a/src/components/main/PageWrapper.tsx b/src/components/main/PageWrapper.tsx index e28891147..cbec93e9e 100644 --- a/src/components/main/PageWrapper.tsx +++ b/src/components/main/PageWrapper.tsx @@ -6,9 +6,9 @@ import Head from 'next/head' import React, { FC } from 'react' import config from 'src/config' import { resolveIpfsUrl } from 'src/ipfs' -import { useMyAddress } from '../auth/MyAccountsContext' -import { useShowOnBoardingSidebarContext } from '../onboarding/contexts/ShowOnBoardingSidebarContext' -import OnBoardingSidebar from '../onboarding/OnBoardingSidebar' +import CreatorDashboardSidebar, { + CreatorDashboardSidebarType, +} from '../creators/CreatorDashboardSidebar' import { useIsMobileWidthOrDevice } from '../responsive' import { fullUrl } from '../urls/helpers' import Section from '../utils/Section' @@ -94,11 +94,14 @@ type Props = { title?: React.ReactNode className?: string outerClassName?: string - withOnBoarding?: boolean + withSidebar?: boolean withVoteBanner?: boolean + creatorDashboardSidebarType?: CreatorDashboardSidebarType } -const ONBOARDING_SIDEBAR_WIDTH = 300 +const SIDEBAR_WIDTH = 300 +// offset for making box shadow of content still visible while having the scrollbar +const BOX_SHADOW_OFFSET = 24 export const PageContent: FC = ({ /* leftPanel, */ meta, @@ -107,11 +110,15 @@ export const PageContent: FC = ({ title, className, outerClassName, - withOnBoarding, + // withSidebar, children, + creatorDashboardSidebarType, }) => { - const { showOnBoardingSidebar, setShowOnBoardingSidebar } = useShowOnBoardingSidebarContext() - const myAddress = useMyAddress() + // const { + // showOnBoardingSidebar, + // // setShowOnBoardingSidebar + // } = useShowOnBoardingSidebarContext() + // const myAddress = useMyAddress() const isMobile = useIsMobileWidthOrDevice() // const isPanels = leftPanel || rightPanel @@ -151,9 +158,22 @@ export const PageContent: FC = ({ {/* {isPanels &&
{rightPanel}
} */} {rightPanel} - {rightPanel === undefined && withOnBoarding && showOnBoardingSidebar && myAddress && ( -
- setShowOnBoardingSidebar(false)} /> + {rightPanel === undefined && creatorDashboardSidebarType && ( +
+ + {/* setShowOnBoardingSidebar(false)} /> */}
)}
diff --git a/src/components/main/types.ts b/src/components/main/types.ts index 59fcbfa5b..509516a48 100644 --- a/src/components/main/types.ts +++ b/src/components/main/types.ts @@ -29,7 +29,7 @@ export type SpaceDateFilterProps = SpaceFilterProps & { filter: SpaceFilterType } -export type TabsWithoutFeed = 'posts' | 'spaces' | 'comments' +export type TabsWithoutFeed = 'posts' | 'spaces' | 'comments' | 'creators' export type TabKeys = TabsWithoutFeed | 'feed' diff --git a/src/components/main/utils.ts b/src/components/main/utils.ts index edc540c39..d78c50ebd 100644 --- a/src/components/main/utils.ts +++ b/src/components/main/utils.ts @@ -51,19 +51,13 @@ const getPostsByFilter: GetEntityFilter = { }, } -const getSpacesByFilter: GetEntityFilter = { +const getSpacesByFilter: GetEntityFilter> = { latest: { day: q.GET_LATEST_SPACE_IDS, week: q.GET_LATEST_SPACE_IDS, month: q.GET_LATEST_SPACE_IDS, allTime: q.GET_LATEST_SPACE_IDS, }, - suggested: { - day: q.GET_LATEST_SPACE_IDS, - week: q.GET_LATEST_SPACE_IDS, - month: q.GET_LATEST_SPACE_IDS, - allTime: q.GET_LATEST_SPACE_IDS, - }, sortByFollowers: { day: q.GET_MOST_FOLLOWED_SPACE_IDS_IN_DATE_RANGE, week: q.GET_MOST_FOLLOWED_SPACE_IDS_IN_DATE_RANGE, @@ -78,7 +72,7 @@ const getSpacesByFilter: GetEntityFilter = { }, } -export const tabs = ['feed', 'posts', 'comments', 'spaces'] +export const tabs = ['feed', 'posts', 'comments', 'spaces', 'creators'] type GetEntityFilter = Record> @@ -119,6 +113,7 @@ export const loadPostsByQuery = createLoadEntitiesByQuery(getPostsByFilter) export const getFilterType = (key: string, type: string | undefined): EntityFilter | undefined => { if (key === 'feed') return + if (filterByKey[key as TabsWithoutFeed].length === 0) return undefined const typeIndex = filterByKey[key as TabsWithoutFeed].findIndex( typeFromObj => typeFromObj.value === type, @@ -141,7 +136,8 @@ export const setTabInUrl = (router: NextRouter, tab: string, queries?: Record `${key}=${value}`) + .map(([key, value]) => !!value && `${key}=${value}`) + .filter(Boolean) .join('&') const asPath = `${router.asPath.split('?')[0]}?${queryStr}` diff --git a/src/components/onboarding/OnBoardingSidebar/OnBoardingSidebar.tsx b/src/components/onboarding/OnBoardingSidebar/OnBoardingSidebar.tsx index d8219b654..c39a28b3e 100644 --- a/src/components/onboarding/OnBoardingSidebar/OnBoardingSidebar.tsx +++ b/src/components/onboarding/OnBoardingSidebar/OnBoardingSidebar.tsx @@ -8,10 +8,8 @@ import config from 'src/config' import { useOpenCloseEnableConfirmationModal } from 'src/rtk/features/confirmationPopup/useOpenCloseEnableConfirmationModal' import { useOpenCloseOnBoardingModal } from 'src/rtk/features/onBoarding/onBoardingHooks' import { OnBoardingDataTypes } from 'src/rtk/features/onBoarding/onBoardingSlice' -import { useIsOnBoardingSkippedContext } from '../contexts/IsOnBoardingSkippedContext' import useOnBoardingStepsOrder from '../hooks/useOnBoardingStepsOrder' import OnBoardingQuickStartModal from '../OnBoardingQuickStartModal' -import ContinueOnBoardingButton from './buttons/ContinueOnBoardingButton' import DotsamaDomainButton from './buttons/DotsamaDomainButton' import WritePostButton from './buttons/WritePostButton' import styles from './OnBoardingSidebar.module.sass' @@ -99,7 +97,7 @@ export default function OnBoardingSidebar({ const openCloseEnableConfirmationModal = useOpenCloseEnableConfirmationModal() const name = useAccountName() - const { isOnBoardingSkipped } = useIsOnBoardingSkippedContext() + // const { isOnBoardingSkipped } = useIsOnBoardingSkippedContext() const isUsingEmail = useIsUsingEmail() const showEnableConfirmationBtn = steps.includes('signer') ? false : true @@ -123,7 +121,7 @@ export default function OnBoardingSidebar({ Start a quick tour
- {isOnBoardingSkipped && steps.length > 0 && } + {/* {isOnBoardingSkipped && steps.length > 0 && } */} {showEnableConfirmationBtn && ( return isMobile ? : } -const CreatePostIcon = +export function CreatePostButtonAndModal({ + children, +}: { + children: (onClick: () => void) => React.ReactNode +}) { + const [visible, setVisible] = useState(false) + const { asPath } = useRouter() + + useEffect(() => { + setVisible(false) + }, [asPath]) + + /** Go to new post form or show the space selector modal. */ + const onNewPostClick = () => { + setVisible(true) + } + + return ( + <> + {children(onNewPostClick)} + {visible && setVisible(false)} />} + + ) +} const NewPostButtonAndModal = () => { const { isMobile } = useResponsiveSize() @@ -47,9 +70,7 @@ const NewPostButtonAndModal = () => { ) : ( - + )} {visible && setVisible(false)} />} @@ -59,9 +80,13 @@ const NewPostButtonAndModal = () => { export function NewPostButtonInTopMenu() { const myAddress = useMyAddress() - const ids = useSelectSpaceIdsWhereAccountCanPost(myAddress) - const spaceIds = selectSpaceIdsThatCanSuggestIfSudo({ myAddress, spaceIds: ids }) + const { isLoading, spaceIds: ids } = + useSelectSpaceIdsWhereAccountCanPostWithLoadingStatus(myAddress) + if (isLoading) { + return null + } + const spaceIds = selectSpaceIdsThatCanSuggestIfSudo({ myAddress, spaceIds: ids }) const anySpace = spaceIds[0] if (!anySpace) return diff --git a/src/components/posts/PostStats.tsx b/src/components/posts/PostStats.tsx index 07dd7a98d..fb65e04a5 100644 --- a/src/components/posts/PostStats.tsx +++ b/src/components/posts/PostStats.tsx @@ -1,10 +1,8 @@ -import { nonEmptyStr } from '@subsocial/utils' import clsx from 'clsx' import { useState } from 'react' import { useSetChatOpen } from 'src/rtk/app/hooks' import { useAppSelector } from 'src/rtk/app/store' import { idToBn, PostStruct } from 'src/types' -import { useResponsiveSize } from '../responsive' import { MutedSpan } from '../utils/MutedText' import { Pluralize } from '../utils/Plularize' import { ActiveVoters, PostVoters } from '../voting/ListVoters' @@ -15,8 +13,7 @@ type StatsProps = { } export const StatsPanel = (props: StatsProps) => { - const { post, goToCommentsId } = props - const { isLargeDesktop } = useResponsiveSize() + const { post } = props const setChatOpen = useSetChatOpen() const [postVotersOpen, setPostVotersOpen] = useState(false) @@ -40,13 +37,7 @@ export const StatsPanel = (props: StatsProps) => { - {!isLargeDesktop && nonEmptyStr(goToCommentsId) ? ( - - {comments} - - ) : ( - {comments} - )} + {comments} { diff --git a/src/components/posts/editor/ModalEditor.tsx b/src/components/posts/editor/ModalEditor.tsx index c2e0bef2c..20854b8a8 100644 --- a/src/components/posts/editor/ModalEditor.tsx +++ b/src/components/posts/editor/ModalEditor.tsx @@ -1,12 +1,14 @@ import { LoadingOutlined } from '@ant-design/icons' import { IpfsContent } from '@subsocial/api/substrate/wrappers' import { newLogger } from '@subsocial/utils' -import { Col, Form, Modal, ModalProps, Row } from 'antd' +import { Button, Col, Form, Modal, ModalProps, Row } from 'antd' import { LabeledValue } from 'antd/lib/select' import clsx from 'clsx' import dynamic from 'next/dynamic' +import Link from 'next/link' import { useRouter } from 'next/router' import { useCallback, useEffect, useState } from 'react' +import { AiFillInfoCircle } from 'react-icons/ai' import { BiImage } from 'react-icons/bi' import { useMyAddress } from 'src/components/auth/MyAccountsContext' import { htmlToMd } from 'src/components/editor/tiptap' @@ -21,8 +23,10 @@ import { ButtonLink } from 'src/components/utils/CustomLinks' import SelectSpacePreview from 'src/components/utils/SelectSpacePreview' import TxButton from 'src/components/utils/TxButton' import { useFetchSpaces, useSelectSpaceIdsWhereAccountCanPost } from 'src/rtk/app/hooks' +import { useFetchTotalStake } from 'src/rtk/features/creators/totalStakeHooks' import { AnyId, DataSourceTypes, IpfsCid, PostContent } from 'src/types' import { selectSpaceIdsThatCanSuggestIfSudo } from 'src/utils' +import { activeStakingLinks, getSubIdCreatorsLink } from 'src/utils/links' import { RegularPostExt } from '.' import { fieldName, FormValues } from './Fileds' import styles from './index.module.sass' @@ -182,9 +186,50 @@ export interface PostEditorModalProps extends Omit { onCancel?: () => void } export const PostEditorModal = (props: PostEditorModalProps) => { + const myAddress = useMyAddress() + const { data } = useFetchTotalStake(myAddress ?? '') + const hasStaked = data?.hasStaked + return ( - - props.onCancel && props.onCancel()} /> + +
+ props.onCancel && props.onCancel()} /> +
+
+
+
+ + Post to Earn +
+ {hasStaked ? ( +

+ You can receive extra SUB when others like your posts. Feel free to share your post to + accumulate more rewards.{' '} + + + How does it work? + + +

+ ) : ( +

+ You can receive extra SUB when others like your posts. However, you need to first + stake some SUB to become eligible. +

+ )} +
+ {!hasStaked && ( + + )} +
) } diff --git a/src/components/posts/editor/index.module.sass b/src/components/posts/editor/index.module.sass index 83626786e..00d010101 100644 --- a/src/components/posts/editor/index.module.sass +++ b/src/components/posts/editor/index.module.sass @@ -165,3 +165,46 @@ .HeaderSafeArea margin-top: 0 margin-bottom: 64px + +.ModalEditor + :global(.ant-modal-content) + background: transparent + box-shadow: none + + .Content + background: white + border-radius: $border_radius_normal + padding: $space_normal + box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05) + + .InfoPanel + background: #EEF2FF + padding: $space_normal + border-radius: $border_radius_large + margin-top: $space_normal + display: flex + align-items: center + gap: $space_normal + + p + margin: 0 + margin-top: $space_mini + font-size: $font_small + opacity: 0.8 + + .InfoPanelContent + display: flex + flex-direction: column + + .Title + display: flex + align-items: center + gap: $space_tiny + + svg + color: #60A5FA + position: relative + top: 1px + + span + font-weight: bold diff --git a/src/components/posts/share/ShareDropdown/index.tsx b/src/components/posts/share/ShareDropdown/index.tsx index 809f4c752..bfd3fe26d 100644 --- a/src/components/posts/share/ShareDropdown/index.tsx +++ b/src/components/posts/share/ShareDropdown/index.tsx @@ -5,11 +5,12 @@ import { LinkedinOutlined, LinkOutlined, RedditOutlined, - ShareAltOutlined, TwitterOutlined, } from '@ant-design/icons' import { Button, Dropdown, Menu } from 'antd' +import clsx from 'clsx' import { useState } from 'react' +import { AiOutlineShareAlt } from 'react-icons/ai' import { copyUrl, facebookShareUrl, @@ -27,7 +28,6 @@ import SharePostLink from '../SharePostLink' type ShareMenuProps = { postDetails: PostWithSomeDetails space?: SpaceStruct - preview?: boolean title?: string className?: string onClick?: FVoid @@ -111,10 +111,12 @@ const ShareMenu = (props: ShareMenuProps) => { ) } -const ShareIcon = +const ShareIcon = ( + +) export const ShareDropdown = (props: ShareMenuProps) => { - const { preview, title = 'Share', className, postDetails } = props + const { title = 'Share', className, postDetails } = props const { post: { struct: { sharesCount }, @@ -131,12 +133,8 @@ export const ShareDropdown = (props: ShareMenuProps) => { placement='bottomCenter' overlay={} > - ) diff --git a/src/components/posts/view-post/PostPage.tsx b/src/components/posts/view-post/PostPage.tsx index 2084045df..87fc93e1f 100644 --- a/src/components/posts/view-post/PostPage.tsx +++ b/src/components/posts/view-post/PostPage.tsx @@ -1,4 +1,4 @@ -import { parseTwitterTextToMarkdown, summarize, summarizeMd } from '@subsocial/utils' +import { parseTwitterTextToMarkdown, summarize } from '@subsocial/utils' import { getPostIdFromSlug } from '@subsocial/utils/slugify' import clsx from 'clsx' import { NextPage } from 'next' @@ -19,14 +19,7 @@ import { useSelectProfile, useSetChatEntityConfig, useSetChatOpen } from 'src/rt import { useAppSelector } from 'src/rtk/app/store' import { fetchPost, fetchPosts, selectPost } from 'src/rtk/features/posts/postsSlice' import { useFetchMyReactionsByPostId } from 'src/rtk/features/reactions/myPostReactionsHooks' -import { - asCommentStruct, - HasStatusCode, - idToBn, - PostData, - PostWithAllDetails, - PostWithSomeDetails, -} from 'src/types' +import { asCommentStruct, HasStatusCode, idToBn, PostData, PostWithSomeDetails } from 'src/types' import { DfImage } from '../../utils/DfImage' import { DfMd } from '../../utils/DfMd' import Section from '../../utils/Section' @@ -47,13 +40,11 @@ import { PostDropDownMenu } from './PostDropDownMenu' import TwitterPost from './TwitterPost' export type PostDetailsProps = { - postData: PostWithAllDetails + postData: PostWithSomeDetails rootPostData?: PostWithSomeDetails statusCode?: number } -const MAX_META_TITLE_LEN = 100 - const InnerPostPage: NextPage = props => { const { postData: initialPostData, rootPostData } = props const id = initialPostData.id @@ -68,7 +59,7 @@ const InnerPostPage: NextPage = props => { const setChatConfig = useSetChatEntityConfig() useEffect(() => { if (!post) return - setChatConfig({ data: post, type: 'post' }) + setChatConfig({ entity: { data: post, type: 'post' }, withFloatingButton: true }) return () => { setChatConfig(null) @@ -125,8 +116,21 @@ const InnerPostPage: NextPage = props => { const titleMsg = struct.isComment ? renderResponseTitle(rootPostData?.post) : title let metaTitle = title const defaultMetaTitle = config.metaTags.title - if (!metaTitle && typeof body === 'string') { - metaTitle = summarizeMd(body, { limit: MAX_META_TITLE_LEN }).summary + + // should forceTitle only when its using the space/owner name, to not include double Polkaverse name + let forceTitle = false + if (!metaTitle) { + const owner = initialPostData.owner + const ownerName = owner?.content?.name.trim() + const ownerHandle = owner?.struct.handle?.trim() + const spaceName = initialPostData.space?.content?.name?.trim() + if (ownerName) { + metaTitle = `${ownerName} ` + (ownerHandle ? `@${ownerHandle} ` : '') + 'on Polkaverse' + forceTitle = true + } else if (spaceName) { + metaTitle = `${spaceName} on Polkaverse` + forceTitle = true + } } let usedImage = image @@ -137,20 +141,22 @@ const InnerPostPage: NextPage = props => { } } + const isSpaceAlreadyRenderedInSidebar = isNotMobile + return (
@@ -205,7 +211,7 @@ const InnerPostPage: NextPage = props => {
)} -
+
= props => { withTipButton />
- {!isSameProfileAndSpace && ( + {!isSameProfileAndSpace && !isSpaceAlreadyRenderedInSidebar && ( )}
@@ -310,7 +316,7 @@ getInitialPropsWithRedux(PostPage, async props => { } return { - postData: data as PostWithAllDetails, + postData: data, rootPostData, } }) diff --git a/src/components/posts/view-post/ViewRegularPreview.tsx b/src/components/posts/view-post/ViewRegularPreview.tsx index 815de2bcd..aa787a146 100644 --- a/src/components/posts/view-post/ViewRegularPreview.tsx +++ b/src/components/posts/view-post/ViewRegularPreview.tsx @@ -24,7 +24,7 @@ export const RegularPreview: ComponentType = props => { withTags={withTags} withMarginForCardType={!withActions} /> - {withActions && } + {withActions && } ) : ( diff --git a/src/components/posts/view-post/ViewSharedPreview.tsx b/src/components/posts/view-post/ViewSharedPreview.tsx index a8d14f816..f295d9015 100644 --- a/src/components/posts/view-post/ViewSharedPreview.tsx +++ b/src/components/posts/view-post/ViewSharedPreview.tsx @@ -21,7 +21,7 @@ export const SharedPreview: ComponentType = props => {
- {withActions && } + {withActions && } ) } diff --git a/src/components/posts/view-post/helpers.tsx b/src/components/posts/view-post/helpers.tsx index 21584bd71..a7bf80fea 100644 --- a/src/components/posts/view-post/helpers.tsx +++ b/src/components/posts/view-post/helpers.tsx @@ -283,35 +283,22 @@ type PostActionsPanelProps = { postDetails: PostWithSomeDetails space?: SpaceStruct toogleCommentSection?: () => void - preview?: boolean withBorder?: boolean + className?: string } export const PostActionsPanel: FC = props => { - const { postDetails, space, preview, withBorder } = props + const { postDetails, space, withBorder, className } = props const { post: { struct }, } = postDetails - const ReactionsAction = () => ( - - ) + const ReactionsAction = () => return ( -
- {preview ? ( - - ) : ( -
- -
- )} - +
+ +
) } diff --git a/src/components/profiles/ViewProfile.tsx b/src/components/profiles/ViewProfile.tsx index ad1bed08f..10672fd87 100644 --- a/src/components/profiles/ViewProfile.tsx +++ b/src/components/profiles/ViewProfile.tsx @@ -198,7 +198,7 @@ const ProfilePage: NextPage = props => { image, canonical: accountUrl({ address }), }} - withOnBoarding + withSidebar > {!shouldHideContent && } diff --git a/src/components/spaces/AboutSpace.tsx b/src/components/spaces/AboutSpace.tsx index fb8f69ea9..1defe3cdd 100644 --- a/src/components/spaces/AboutSpace.tsx +++ b/src/components/spaces/AboutSpace.tsx @@ -53,7 +53,7 @@ export const InnerAboutSpacePage: NextPage = ({ spaceData }) => { } return ( - +
{nonEmptyStr(about) && (
diff --git a/src/components/spaces/AccountSpaces.tsx b/src/components/spaces/AccountSpaces.tsx index 3cf18ae66..ceb5e1de2 100644 --- a/src/components/spaces/AccountSpaces.tsx +++ b/src/components/spaces/AccountSpaces.tsx @@ -173,7 +173,7 @@ export const OwnedSpacesPage = () => { title: 'Account spaces', desc: `Subsocial spaces owned by ${address}`, }} - withOnBoarding + withSidebar > @@ -211,7 +211,7 @@ export const FollowingSpacesPage = () => { title: `Subscriptions of ${address}`, desc: `Spaces that ${address} follows on Subsocial`, }} - withOnBoarding + withSidebar > diff --git a/src/components/spaces/LatestSpacesPage.tsx b/src/components/spaces/LatestSpacesPage.tsx index 31976a8e9..e549b3435 100644 --- a/src/components/spaces/LatestSpacesPage.tsx +++ b/src/components/spaces/LatestSpacesPage.tsx @@ -1,9 +1,13 @@ +import shuffle from 'lodash.shuffle' import { FC, useCallback } from 'react' import { useDispatch } from 'react-redux' import { useSubsocialApi } from 'src/components/substrate/SubstrateContext' import config from 'src/config' import { useDfApolloClient } from 'src/graphql/ApolloProvider' import { GetLatestSpaceIds } from 'src/graphql/__generated__/GetLatestSpaceIds' +import { useAppDispatch } from 'src/rtk/app/store' +import { useFetchCreators } from 'src/rtk/features/creators/creatorsListHooks' +import { fetchCreators } from 'src/rtk/features/creators/creatorsListSlice' import { fetchMyPermissionsBySpaceIds } from 'src/rtk/features/permissions/mySpacePermissionsSlice' import { DataSourceTypes, SpaceId } from 'src/types' import { fetchSpaces } from '../../rtk/features/spaces/spacesSlice' @@ -18,27 +22,42 @@ import { PublicSpacePreviewById } from './SpacePreview' const { recommendedSpaceIds } = config type Props = { + spaceIds?: SpaceId[] initialSpaceIds?: SpaceId[] + customFetcher?: (config: LoadMoreValues) => Promise totalSpaceCount: number filter: SpaceFilterType dateFilter?: DateFilterType } -const loadMoreSpacesFn = async (loadMoreValues: LoadMoreValues) => { - const { client, size, page, myAddress, subsocial, dispatch, filter } = loadMoreValues +const loadMoreSpacesFn = async ( + loadMoreValues: LoadMoreValues & { + customFetcher?: (config: LoadMoreValues) => Promise + }, +) => { + const { client, size, page, myAddress, subsocial, dispatch, filter, customFetcher } = + loadMoreValues - if (filter.type === undefined) return [] + if (filter === undefined) return [] let spaceIds: string[] = [] - if (!isSuggested(filter.type) && client) { - const offset = (page - 1) * size - const data = await loadSpacesByQuery({ client, offset, filter }) - - const { spaces } = data as GetLatestSpaceIds - spaceIds = spaces.map(value => value.id) + if (customFetcher) { + spaceIds = await customFetcher(loadMoreValues) } else { - spaceIds = getPageOfIds(recommendedSpaceIds, { page, size }) + if (filter.type !== 'suggested' && client) { + const offset = (page - 1) * size + const data = await loadSpacesByQuery({ + client, + offset, + filter: { type: filter.type, date: filter.date }, + }) + + const { spaces } = data as GetLatestSpaceIds + spaceIds = spaces.map(value => value.id) + } else { + spaceIds = getPageOfIds(recommendedSpaceIds, { page, size }) + } } await Promise.all([ @@ -50,14 +69,14 @@ const loadMoreSpacesFn = async (loadMoreValues: LoadMoreValues) } const InfiniteListOfSpaces = (props: Props) => { - const { totalSpaceCount, initialSpaceIds, filter, dateFilter } = props + const { totalSpaceCount, initialSpaceIds, filter, dateFilter, customFetcher } = props const client = useDfApolloClient() const dispatch = useDispatch() const { subsocial } = useSubsocialApi() const myAddress = useMyAddress() - const loadMore: InnerLoadMoreFn = (page, size) => - loadMoreSpacesFn({ + const loadMore: InnerLoadMoreFn = (page, size) => { + return loadMoreSpacesFn({ client, size, page, @@ -68,7 +87,9 @@ const InfiniteListOfSpaces = (props: Props) => { type: filter, date: dateFilter, }, + customFetcher, }) + } const List = useCallback( () => ( @@ -100,4 +121,27 @@ export const SuggestedSpaces = () => ( ) +let shuffledCreators: string[] | null = null +export const CreatorsSpaces = () => { + const { data: creators } = useFetchCreators() + + const dispatch = useAppDispatch() + const loadCreators = async () => { + if (shuffledCreators) return shuffledCreators + + const res = await dispatch(fetchCreators({})) + const spaceIds = (res.payload as { spaceId: string }[]).map(({ spaceId }) => spaceId) + shuffledCreators = shuffle(spaceIds) + return shuffledCreators + } + return ( + + ) +} + export default LatestSpacesPage diff --git a/src/components/spaces/SpacePreview.tsx b/src/components/spaces/SpacePreview.tsx index 90bc3c202..c499e4ada 100644 --- a/src/components/spaces/SpacePreview.tsx +++ b/src/components/spaces/SpacePreview.tsx @@ -10,7 +10,7 @@ type PreviewProps = { } export const SpacePreview = ({ space }: PreviewProps) => ( - + ) type PublicSpacePreviewByIdProps = { diff --git a/src/components/spaces/UnclaimedSpacesList copy.tsx b/src/components/spaces/UnclaimedSpacesList copy.tsx index 595a0433a..5592e272d 100644 --- a/src/components/spaces/UnclaimedSpacesList copy.tsx +++ b/src/components/spaces/UnclaimedSpacesList copy.tsx @@ -51,7 +51,7 @@ const UnclaimedSpacesListPage = () => { title, desc: 'Only unclaimed spaces of polka', }} - withOnBoarding + withSidebar > diff --git a/src/components/spaces/UnclaimedSpacesList.tsx b/src/components/spaces/UnclaimedSpacesList.tsx index 196c55963..0a5f0214d 100644 --- a/src/components/spaces/UnclaimedSpacesList.tsx +++ b/src/components/spaces/UnclaimedSpacesList.tsx @@ -47,7 +47,7 @@ const UnclaimedSpacesListPage: FC = props => { desc: 'Only unclaimed spaces of Polkadot projects', }} title={title} - withOnBoarding + withSidebar >

diff --git a/src/components/spaces/ViewSpace.tsx b/src/components/spaces/ViewSpace.tsx index dbb486f01..6d2336bc5 100644 --- a/src/components/spaces/ViewSpace.tsx +++ b/src/components/spaces/ViewSpace.tsx @@ -1,16 +1,22 @@ import { EditOutlined } from '@ant-design/icons' import { isEmptyStr, newLogger, nonEmptyStr } from '@subsocial/utils' +import { Button } from 'antd' import clsx from 'clsx' import dynamic from 'next/dynamic' -import React, { MouseEvent, useCallback, useState } from 'react' +import React, { MouseEvent, useCallback, useEffect, useState } from 'react' import { ButtonLink } from 'src/components/utils/CustomLinks' import { Segment } from 'src/components/utils/Segment' import { LARGE_AVATAR_SIZE } from 'src/config/Size.config' -import { SpaceContent, SpaceId, SpaceStruct, SpaceWithSomeDetails } from 'src/types' -import config from '../../config' +import { useSetChatEntityConfig, useSetChatOpen } from 'src/rtk/app/hooks' +import { useIsCreatorSpace } from 'src/rtk/features/creators/creatorsListHooks' +import { useFetchStakeData } from 'src/rtk/features/creators/stakesHooks' +import { SpaceContent, SpaceData, SpaceId, SpaceStruct, SpaceWithSomeDetails } from 'src/types' import { useSelectProfileSpace } from '../../rtk/features/profiles/profilesHooks' import { useSelectSpace } from '../../rtk/features/spaces/spacesHooks' import { useMyAddress } from '../auth/MyAccountsContext' +import MyStakeCard from '../creators/cards/MyStakeCard' +import StakeSubCard from '../creators/cards/StakeSubCard' +import MobileIncreaseSubRewards from '../creators/MobileIncreaseSubRewards' import MakeAsProfileModal from '../profiles/address-views/utils/MakeAsProfileModal' import { useIsMobileWidthOrDevice } from '../responsive' import { editSpaceUrl, spaceUrl } from '../urls' @@ -60,7 +66,8 @@ export const SpaceNameAsLink = React.memo(({ space, ...props }: SpaceNameAsLinkP }) export const StakeButton = ({ spaceStruct }: { spaceStruct: SpaceStruct }) => { - return config.creatorIds?.includes(spaceStruct.id) ? ( + const { isCreatorSpace } = useIsCreatorSpace(spaceStruct.id) + return isCreatorSpace ? ( { withFollowButton = true, withStats = true, withTags = true, - withStakeButton = true, showFullAbout = false, dropdownPreview = false, @@ -115,6 +121,19 @@ export const InnerViewSpace = (props: Props) => { ) }, [spaceData, imageSize]) + const setChatConfig = useSetChatEntityConfig() + const setChatOpen = useSetChatOpen() + useEffect(() => { + if (!spaceData) return + setChatConfig({ entity: { data: spaceData, type: 'space' }, withFloatingButton: false }) + + return () => { + setChatConfig(null) + } + }, [spaceData]) + + const { isCreatorSpace } = useIsCreatorSpace(spaceData?.id) + // We do not return 404 page here, because this component could be used to render a space in list. if (!spaceData) return null @@ -178,6 +197,10 @@ export const InnerViewSpace = (props: Props) => { e.stopPropagation() setCollapseAbout(prev => !prev) } + const toggleCreatorChat = () => { + setChatOpen(true) + } + const renderPreview = () => (

@@ -185,26 +208,25 @@ export const InnerViewSpace = (props: Props) => {
{title} - - - {!isMobile && - (isMy ? ( - - Edit - - ) : ( - withStakeButton && - ))} - - {withFollowButton && } + + + {!isMobile && isMy && ( + + Edit + + )} + + {!isMobile && isCreatorSpace && ( + + )} + + {withFollowButton && }
@@ -276,11 +298,17 @@ export const InnerViewSpace = (props: Props) => { ) } + const showCreatorCards = isCreatorSpace && isMobile + return (
+ {showCreatorCards && ( + + )}
{renderPreview()}
+ {showCreatorCards && }
@@ -289,6 +317,17 @@ export const InnerViewSpace = (props: Props) => { ) } +function MobileCreatorCard({ spaceData }: { spaceData: SpaceData }) { + const myAddress = useMyAddress() + const { data } = useFetchStakeData(myAddress ?? '', spaceData.id) + + return ( +
+ {data?.hasStaked ? : } +
+ ) +} + export const ViewSpace = (props: Props) => { const { spaceData: initialSpaceData } = props diff --git a/src/components/spaces/ViewSpacePage.tsx b/src/components/spaces/ViewSpacePage.tsx index 3cc2fb5e7..aa1d2de54 100644 --- a/src/components/spaces/ViewSpacePage.tsx +++ b/src/components/spaces/ViewSpacePage.tsx @@ -48,8 +48,9 @@ const InnerViewSpacePage: FC = props => { image, canonical: spaceUrl(spaceData.struct), }} - withOnBoarding + withSidebar withVoteBanner + creatorDashboardSidebarType={{ name: 'space-page', space: spaceData }} > {showBanner && ( - {children || ( - - {getTitle(asProfile)} - - )} + {children || {getTitle(asProfile)}} ) } diff --git a/src/components/spaces/helpers/PostPreviewsOnSpace.tsx b/src/components/spaces/helpers/PostPreviewsOnSpace.tsx index 9fe82e3c5..a7f35e9cd 100644 --- a/src/components/spaces/helpers/PostPreviewsOnSpace.tsx +++ b/src/components/spaces/helpers/PostPreviewsOnSpace.tsx @@ -1,11 +1,10 @@ -import React, { useCallback } from 'react' +import { useCallback } from 'react' import { useDispatch } from 'react-redux' import { InnerLoadMoreFn } from 'src/components/lists' import { InfinitePageList } from 'src/components/lists/InfiniteList' import { PublicPostPreviewById } from 'src/components/posts/PublicPostPreview' import { useSubsocialApi } from 'src/components/substrate/SubstrateContext' import { getPageOfIds } from 'src/components/utils/getIds' -import { Pluralize } from 'src/components/utils/Plularize' import { fetchPosts } from 'src/rtk/features/posts/postsSlice' import { DataSourceTypes, PostId, PostWithSomeDetails, SpaceData } from 'src/types' import { FollowerCanPostAlert } from '../permissions/FollowerCanPostAlert' @@ -17,21 +16,6 @@ type Props = { posts: PostWithSomeDetails[] } -const PostsSectionTitle = React.memo((props: Props) => { - const { spaceData } = props - const { struct: space } = spaceData - const { postsCount } = space - - return ( -
- - - - {!!postsCount && } -
- ) -}) - const InfiniteListOfPublicPosts = (props: Props) => { const { spaceData, posts, postIds } = props const { struct: space } = spaceData @@ -54,7 +38,6 @@ const InfiniteListOfPublicPosts = (props: Props) => { () => ( } dataSource={initialPostIds} loadMore={loadMore} totalCount={postsCount || 0} diff --git a/src/components/spaces/helpers/SpaceDropdownMenu.tsx b/src/components/spaces/helpers/SpaceDropdownMenu.tsx index 37af6b1b3..72b7f2901 100644 --- a/src/components/spaces/helpers/SpaceDropdownMenu.tsx +++ b/src/components/spaces/helpers/SpaceDropdownMenu.tsx @@ -7,6 +7,9 @@ import { BasicDropDownMenuProps, DropdownMenu } from 'src/components/utils/DropD import { showSuccessMessage } from 'src/components/utils/Message' import { useHasUserASpacePermission } from 'src/permissions/checkPermission' import { useSendEvent } from 'src/providers/AnalyticContext' +import { useSetChatOpen } from 'src/rtk/app/hooks' +import { useAppSelector } from 'src/rtk/app/store' +import { useIsCreatorSpace } from 'src/rtk/features/creators/creatorsListHooks' import { SpaceData } from 'src/types' import { useSelectProfile } from '../../../rtk/features/profiles/profilesHooks' import { useIsUsingEmail, useMyAddress } from '../../auth/MyAccountsContext' @@ -40,12 +43,20 @@ export const SpaceDropdownMenu = (props: SpaceDropDownProps) => { const showMakeAsProfileButton = isMySpace && (!profileSpaceId || profileSpaceId !== id) const sendEvent = useSendEvent() + const { isCreatorSpace } = useIsCreatorSpace(struct.id) + const hasChatSetup = useAppSelector(state => !!state.chat.entity) + const setChatOpen = useSetChatOpen() const buildMenuItems = () => { sendEvent('open_space_dropdown_menu') return ( <> + {isCreatorSpace && hasChatSetup && ( + setChatOpen(true)}> + Creator chat + + )} {isMySpace && ( diff --git a/src/components/spaces/helpers/loadSpaceOnNextReq.ts b/src/components/spaces/helpers/loadSpaceOnNextReq.ts index 951d4c664..43ca016b9 100644 --- a/src/components/spaces/helpers/loadSpaceOnNextReq.ts +++ b/src/components/spaces/helpers/loadSpaceOnNextReq.ts @@ -29,7 +29,7 @@ export async function loadSpaceOnNextReq( id: idStr, reload: true, eagerLoadHandles: true, - dataSource: DataSourceTypes.SQUID, + dataSource: DataSourceTypes.CHAIN, }), ) const spaceData = selectSpace(reduxStore.getState(), { id: idStr }) diff --git a/src/components/utils/CollapsibleParagraph/CollapsibleParagraph.module.sass b/src/components/utils/CollapsibleParagraph/CollapsibleParagraph.module.sass new file mode 100644 index 000000000..12a476466 --- /dev/null +++ b/src/components/utils/CollapsibleParagraph/CollapsibleParagraph.module.sass @@ -0,0 +1,3 @@ +.CollapsibleParagraph + p + margin-bottom: 0 \ No newline at end of file diff --git a/src/components/utils/CollapsibleParagraph/CollapsibleParagraph.tsx b/src/components/utils/CollapsibleParagraph/CollapsibleParagraph.tsx new file mode 100644 index 000000000..1ac94008e --- /dev/null +++ b/src/components/utils/CollapsibleParagraph/CollapsibleParagraph.tsx @@ -0,0 +1,44 @@ +import { summarizeMd } from '@subsocial/utils' +import clsx from 'clsx' +import { ComponentProps, useMemo, useState } from 'react' +import { DfMd } from '../DfMd' +import { SummarizeMd } from '../md' +import styles from './CollapsibleParagraph.module.sass' + +export type CollapsibleParagraphProps = ComponentProps<'span'> & { + text: string +} + +export default function CollapsibleParagraph({ text, ...props }: CollapsibleParagraphProps) { + const [collapseAbout, setCollapseAbout] = useState(true) + const summarized = useMemo(() => summarizeMd(text), [text]) + + const onToggleShow = (e: any) => { + e.preventDefault() + e.stopPropagation() + setCollapseAbout(prev => !prev) + } + + return ( + + {!collapseAbout ? ( + <> + +
+ Show Less +
+ + ) : ( + + Show More +
+ } + /> + )} + + ) +} diff --git a/src/components/utils/DfMd.tsx b/src/components/utils/DfMd.tsx index d38091dd2..2df5ba5e5 100644 --- a/src/components/utils/DfMd.tsx +++ b/src/components/utils/DfMd.tsx @@ -1,15 +1,17 @@ +import clsx from 'clsx' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' interface Props { source?: string className?: string + omitDefaultClassName?: boolean } -export const DfMd = ({ source, className = '' }: Props) => ( +export const DfMd = ({ source, omitDefaultClassName, className = '' }: Props) => ( {source?.replaceAll('(https://app.subsocial.network/ipfs', '(https://ipfs.subsocial.network') ?? diff --git a/src/components/utils/OffchainUtils.ts b/src/components/utils/OffchainUtils.ts index c991332fe..cc3354be4 100644 --- a/src/components/utils/OffchainUtils.ts +++ b/src/components/utils/OffchainUtils.ts @@ -59,6 +59,36 @@ export const getChainsInfo = async () => { return res?.data } +export const getCreatorList = async () => { + const res = await axiosRequest(`${subIdApiUrl}/staking/creator/list`) + const creators = (res?.data as { spaceId: string; status: 'Active' | '' }[]) || [] + return creators.filter(({ status }) => status === 'Active').map(({ spaceId }) => ({ spaceId })) +} + +export const getTotalStake = async ({ address }: { address: string }) => { + const res = await axiosRequest(`${subIdApiUrl}/staking/creator/backer/ledger?account=${address}`) + const totalStake = (res?.data?.totalLocked as string) || '' + const stakeAmount = BigInt(totalStake) + + return { amount: stakeAmount.toString(), hasStaked: stakeAmount > 0 } +} + +export const getStakeAmount = async ({ + address, + spaceId, +}: { + spaceId: string + address: string +}) => { + const res = await axiosRequest( + `${subIdApiUrl}/staking/creator/backer/info?account=${address}&ids=${spaceId}`, + ) + const newestStakeInfo = (res?.data?.[spaceId]?.[0] as { staked: string; era: number }) || {} + const stakeAmount = BigInt(newestStakeInfo.staked) + + return { stakeAmount: stakeAmount.toString(), hasStaked: stakeAmount > 0 } +} + type BalanceByNetworkProps = { account: AccountId network: string diff --git a/src/components/utils/md/SummarizeMd.tsx b/src/components/utils/md/SummarizeMd.tsx index ffe5578a9..965250aa1 100644 --- a/src/components/utils/md/SummarizeMd.tsx +++ b/src/components/utils/md/SummarizeMd.tsx @@ -12,10 +12,18 @@ type Props = { content?: SummarizedContent limit?: number more?: JSX.Element + omitDefaultClassName?: boolean } & Omit, 'content'> export const SummarizeMd = React.memo((props: Props) => { - const { content, limit: initialLimit, more, className, ...otherProps } = props + const { + content, + limit: initialLimit, + more, + className, + omitDefaultClassName, + ...otherProps + } = props const { summary: initialSummary = '', isShowMore: initialIsShowMore = false } = content || {} const isMobile = useIsMobileWidthOrDevice() @@ -29,7 +37,7 @@ export const SummarizeMd = React.memo((props: Props) => { if (isEmptyStr(summary)) return null return ( -
+
{summary} {isShowMore && ( e.stopPropagation()}> diff --git a/src/components/voting/VoterButtons.tsx b/src/components/voting/VoterButtons.tsx index bdf6adfb2..05cefdd1e 100644 --- a/src/components/voting/VoterButtons.tsx +++ b/src/components/voting/VoterButtons.tsx @@ -1,7 +1,9 @@ -import { DislikeOutlined, DislikeTwoTone, LikeOutlined, LikeTwoTone } from '@ant-design/icons' +import { DislikeOutlined, DislikeTwoTone } from '@ant-design/icons' import { ButtonProps } from 'antd/lib/button' +import clsx from 'clsx' import dynamic from 'next/dynamic' import React, { CSSProperties } from 'react' +import { AiFillHeart, AiOutlineHeart } from 'react-icons/ai' import { useCreateReloadPost, useCreateUpsertPost } from 'src/rtk/app/hooks' import { useAppSelector } from 'src/rtk/app/store' import { useCreateUpsertMyReaction } from 'src/rtk/features/reactions/myPostReactionsHooks' @@ -15,7 +17,6 @@ import { ReactionType, } from 'src/types' import { useMyAddress } from '../auth/MyAccountsContext' -import { useResponsiveSize } from '../responsive' import { getNewIdsFromEvent } from '../substrate' import { IconWithLabel } from '../utils' import { BareProps } from '../utils/types' @@ -25,7 +26,6 @@ const TxButton = dynamic(() => import('../utils/TxButton'), { ssr: false }) type VoterProps = BareProps & { post: PostStruct - preview?: boolean } type VoterButtonProps = VoterProps & @@ -33,7 +33,6 @@ type VoterButtonProps = VoterProps & reactionEnum: ReactionEnum reaction?: ReactionStruct onSuccess?: () => void - preview?: boolean } const VoterButton = React.memo( @@ -44,11 +43,9 @@ const VoterButton = React.memo( className, style, onSuccess, - preview, disabled, }: VoterButtonProps) => { const { id: postId, upvotesCount, downvotesCount } = post - const { isMobile } = useResponsiveSize() const upsertMyReaction = useCreateUpsertMyReaction() const upsertPost = useCreateUpsertPost() @@ -76,8 +73,7 @@ const VoterButton = React.memo( } const isActive = oldKind === newKind - - const color = isUpvote ? '#00a500' : '#ff0000' + const color = isUpvote ? '#eb2f96' : '#ff0000' const changeReactionTx = isActive ? 'reactions.deletePostReaction' @@ -99,27 +95,27 @@ const VoterButton = React.memo( } let icon: JSX.Element - const label = preview || isMobile ? undefined : newKind + const labelText = isUpvote ? 'Like' : 'Dislike' if (isUpvote) { // offsets is based on icon, use em to recalculate based on icon's font-size. const upvoteButtonStyle: CSSProperties = { position: 'relative', top: '0.07em' } icon = isActive ? ( - + ) : ( - + ) } else { const downvoteButtonStyle: CSSProperties = { position: 'relative', top: '0.21em' } icon = isActive ? ( - + ) : ( - + ) } return ( - + ) }, @@ -159,7 +155,7 @@ const InnerVoterButtons = (props: InnerVoterButtonsProps) => { return ( <> - + {/* */} ) } diff --git a/src/config/app/polkaverse/index.ts b/src/config/app/polkaverse/index.ts index fc71713d7..e1d222bbb 100644 --- a/src/config/app/polkaverse/index.ts +++ b/src/config/app/polkaverse/index.ts @@ -23,23 +23,6 @@ const index: AppConfig = { claimedSpaceIds: ['1', '2', '3', '4', '5'], recommendedSpaceIds: polkaverseSpaces, suggestedTlds: ['sub', 'polka'], - creatorIds: [ - '11414', - '4809', - '4777', - '6953', - '10132', - '6283', - '11581', - '7366', - '11157', - '11566', - '10124', - '11581', - '1573', - '1238', - '11844', - ], } export default index diff --git a/src/config/types.ts b/src/config/types.ts index 4d628480f..5718932a1 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -100,7 +100,6 @@ export type AppConfig = { claimedSpaceIds: SpaceId[] recommendedSpaceIds: SpaceId[] suggestedTlds?: string[] - creatorIds?: string[] } export type CommonSubsocialFeatures = { diff --git a/src/layout/Navigation.tsx b/src/layout/Navigation.tsx index ec9d27a93..b269ed19e 100644 --- a/src/layout/Navigation.tsx +++ b/src/layout/Navigation.tsx @@ -6,8 +6,6 @@ import styles from './Sider.module.sass' import clsx from 'clsx' import dynamic from 'next/dynamic' import { useRouter } from 'next/router' -import ChatSidePanel from 'src/components/chat/ChatSidePanel' -import { useResponsiveSize } from 'src/components/responsive' const TopMenu = dynamic(() => import('./TopMenu'), { ssr: false }) const Menu = dynamic(() => import('./SideMenu'), { ssr: false }) @@ -70,16 +68,12 @@ export const Navigation = (props: Props): JSX.Element => { const { state: { asDrawer }, } = useSidebarCollapsed() - const { isLargeDesktop } = useResponsiveSize() - const { pathname } = useRouter() const content = useMemo( () => {children}, [children], ) - const isPostPage = pathname === '/[spaceId]/[slug]' - return (
@@ -88,7 +82,6 @@ export const Navigation = (props: Props): JSX.Element => { {asDrawer ? : } {content} - {isLargeDesktop && isPostPage && } ) diff --git a/src/rtk/app/hooksCommon.ts b/src/rtk/app/hooksCommon.ts index eeb3aad10..246ba1f46 100644 --- a/src/rtk/app/hooksCommon.ts +++ b/src/rtk/app/hooksCommon.ts @@ -1,6 +1,6 @@ import { AsyncThunkAction } from '@reduxjs/toolkit' import { isEmptyArray, newLogger } from '@subsocial/utils' -import { useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { shallowEqual } from 'react-redux' import useSubsocialEffect from 'src/components/api/useSubsocialEffect' import { @@ -41,6 +41,52 @@ export type FetchOneFn = FetchFn, Struct> const log = newLogger('useFetchEntities') +export function useFetchWithoutApi( + fetch: FetchFn, + args: Args, + config?: { enabled?: boolean }, +): CommonResult { + const { enabled } = config || {} + + const [loading, setLoading] = useState(true) + const [error, setError] = useState() + const dispatch = useAppDispatch() + + const jsonArgs = useMemo(() => { + return JSON.stringify(args) + }, [args]) + + const isEnabled = !!enabled + useEffect(() => { + if (!isEnabled) return + + let isMounted = true + setError(undefined) + + dispatch(fetch(args)) + .catch(err => { + if (isMounted) { + setError(err) + log.error(error) + } + }) + .finally(() => { + if (isMounted) { + setLoading(false) + } + }) + + return () => { + isMounted = false + } + }, [dispatch, jsonArgs, isEnabled]) + + return { + loading, + error, + } +} + export function useFetch( fetch: FetchFn, args: Omit | Partial, diff --git a/src/rtk/app/rootReducer.ts b/src/rtk/app/rootReducer.ts index aedf9fcfc..a9d1d2ab3 100644 --- a/src/rtk/app/rootReducer.ts +++ b/src/rtk/app/rootReducer.ts @@ -5,6 +5,9 @@ import chainsInfo from '../features/chainsInfo/chainsInfoSlice' import chat from '../features/chat/chatSlice' import enableConfirmation from '../features/confirmationPopup/enableConfirmationSlice' import contents from '../features/contents/contentsSlice' +import creatorsList from '../features/creators/creatorsListSlice' +import stakes from '../features/creators/stakesSlice' +import totalStake from '../features/creators/totalStakeSlice' import ordersById from '../features/domainPendingOrders/pendingOrdersSlice' import domainByOwner from '../features/domains/domainsByOwnerSlice' import domains from '../features/domains/domainsSlice' @@ -49,6 +52,9 @@ const rootReducer = combineReducers({ sellerConfig, enableConfirmation, chat, + stakes, + totalStake, + creatorsList, }) export type RootState = ReturnType diff --git a/src/rtk/app/wrappers.ts b/src/rtk/app/wrappers.ts index 397ba9f46..6fcb76ec8 100644 --- a/src/rtk/app/wrappers.ts +++ b/src/rtk/app/wrappers.ts @@ -1,6 +1,7 @@ import { ApolloClient, NormalizedCacheObject } from '@apollo/client' -import { AsyncThunkPayloadCreator, Dictionary, EntityId } from '@reduxjs/toolkit' +import { AsyncThunkPayloadCreator, createAsyncThunk, Dictionary, EntityId } from '@reduxjs/toolkit' import { isDef } from '@subsocial/utils' +import sortKeysRecursive from 'sort-keys-recursive' import { getApolloClient } from 'src/graphql/client' import { DataSourceTypes } from 'src/types' import { @@ -186,3 +187,41 @@ export function generatePrefetchDataFn({ + sliceName, + getCachedData, + fetchData, + saveToCacheAction, + shouldFetchCondition, +}: { + sliceName: string + getCachedData: (state: RootState, args: Args) => ReturnValue | undefined + saveToCacheAction: (data: ReturnValue) => any + fetchData: (args: Args) => Promise + shouldFetchCondition?: (cachedData: ReturnValue | undefined) => boolean +}) { + const currentlyFetchingMap = new Map>() + return createAsyncThunk( + `${sliceName}/fetchOne`, + async (allArgs, { getState, dispatch }): Promise => { + const { reload, ...args } = allArgs + const id = JSON.stringify(sortKeysRecursive(args)) + if (!reload) { + const fetchedData = getCachedData(getState(), allArgs) + if (fetchedData && !shouldFetchCondition?.(fetchedData)) return fetchedData + } + const alreadyFetchedPromise = currentlyFetchingMap.get(id) + if (alreadyFetchedPromise) return alreadyFetchedPromise + + const promise = fetchData(allArgs) + currentlyFetchingMap.set(id, promise) + const res = await promise + + currentlyFetchingMap.delete(id) + await dispatch(saveToCacheAction(res)) + + return promise + }, + ) +} diff --git a/src/rtk/features/chat/chatSlice.ts b/src/rtk/features/chat/chatSlice.ts index a635777c9..b593c6c0b 100644 --- a/src/rtk/features/chat/chatSlice.ts +++ b/src/rtk/features/chat/chatSlice.ts @@ -1,22 +1,30 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { PostData } from '@subsocial/api/types' +import { PostData, SpaceData } from '@subsocial/api/types' const sliceName = 'chats' -type Entity = { - type: 'post' - data: PostData -} | null +type Entity = + | { + type: 'post' + data: PostData + } + | { + type: 'space' + data: SpaceData + } + | null export interface ChatEntity { isOpen: boolean entity: Entity totalMessageCount: number + withFloatingButton?: boolean } const initialState: ChatEntity = { isOpen: false, entity: null, totalMessageCount: 0, + withFloatingButton: false, } const slice = createSlice({ @@ -26,8 +34,12 @@ const slice = createSlice({ setChatOpen: (state, action: PayloadAction) => { state.isOpen = action.payload }, - setChatConfig: (state, action: PayloadAction) => { - state.entity = action.payload + setChatConfig: ( + state, + action: PayloadAction<{ entity: Entity; withFloatingButton: boolean } | null>, + ) => { + state.entity = action?.payload?.entity ?? null + state.withFloatingButton = action?.payload?.withFloatingButton state.totalMessageCount = 0 }, setTotalMessageCount: (state, action: PayloadAction) => { diff --git a/src/rtk/features/creators/creatorsListHooks.ts b/src/rtk/features/creators/creatorsListHooks.ts new file mode 100644 index 000000000..ce7279549 --- /dev/null +++ b/src/rtk/features/creators/creatorsListHooks.ts @@ -0,0 +1,21 @@ +import { useFetchWithoutApi } from 'src/rtk/app/hooksCommon' +import { useAppSelector } from 'src/rtk/app/store' +import { fetchCreators, selectCreators } from './creatorsListSlice' + +const emptyArgs = {} +export function useFetchCreators(config?: { enabled?: boolean }) { + const { enabled } = config || {} + const data = useAppSelector(state => selectCreators(state)) + + const props = useFetchWithoutApi(fetchCreators, emptyArgs, { enabled }) + + return { + ...props, + data, + } +} + +export function useIsCreatorSpace(spaceId?: string) { + const { data, loading } = useFetchCreators({ enabled: !!spaceId }) + return { isCreatorSpace: data.map(({ spaceId }) => spaceId).includes(spaceId ?? ''), loading } +} diff --git a/src/rtk/features/creators/creatorsListSlice.ts b/src/rtk/features/creators/creatorsListSlice.ts new file mode 100644 index 000000000..60c24b8be --- /dev/null +++ b/src/rtk/features/creators/creatorsListSlice.ts @@ -0,0 +1,35 @@ +import { createEntityAdapter, createSlice } from '@reduxjs/toolkit' +import { getCreatorList } from 'src/components/utils/OffchainUtils' +import { RootState } from 'src/rtk/app/rootReducer' +import { createSimpleFetchWrapper } from 'src/rtk/app/wrappers' + +export type Creator = { spaceId: string } +const sliceName = 'creatorsList' + +const adapter = createEntityAdapter({ + selectId: data => data.spaceId, +}) +const selectors = adapter.getSelectors(state => state.creatorsList) + +export const selectCreators = selectors.selectAll + +export const fetchCreators = createSimpleFetchWrapper<{}, Creator[]>({ + sliceName, + fetchData: async function () { + const data = await getCreatorList() + return data + }, + saveToCacheAction: data => slice.actions.setCreators(data), + getCachedData: state => selectCreators(state), + shouldFetchCondition: cachedData => !cachedData?.length, +}) + +const slice = createSlice({ + name: sliceName, + initialState: adapter.getInitialState(), + reducers: { + setCreators: adapter.setAll, + }, +}) + +export default slice.reducer diff --git a/src/rtk/features/creators/stakesHooks.ts b/src/rtk/features/creators/stakesHooks.ts new file mode 100644 index 000000000..d013d2bdf --- /dev/null +++ b/src/rtk/features/creators/stakesHooks.ts @@ -0,0 +1,25 @@ +import { useMemo } from 'react' +import { useFetchWithoutApi } from 'src/rtk/app/hooksCommon' +import { useAppSelector } from 'src/rtk/app/store' +import { fetchStakeData, getStakeId, selectStakeForCreator } from './stakesSlice' + +export function useStakeData(address: string, creatorSpaceId: string) { + return useAppSelector(state => + selectStakeForCreator(state, getStakeId({ address, creatorSpaceId })), + ) +} + +export function useFetchStakeData(address: string, creatorSpaceId: string) { + const data = useStakeData(address, creatorSpaceId) + + const args = useMemo(() => { + return { address, creatorSpaceId } + }, [address, creatorSpaceId]) + + const props = useFetchWithoutApi(fetchStakeData, args, { enabled: !!address && !!creatorSpaceId }) + + return { + ...props, + data, + } +} diff --git a/src/rtk/features/creators/stakesSlice.ts b/src/rtk/features/creators/stakesSlice.ts new file mode 100644 index 000000000..352a55bf0 --- /dev/null +++ b/src/rtk/features/creators/stakesSlice.ts @@ -0,0 +1,54 @@ +import { createEntityAdapter, createSlice } from '@reduxjs/toolkit' +import { getStakeAmount } from 'src/components/utils/OffchainUtils' +import { RootState } from 'src/rtk/app/rootReducer' +import { createSimpleFetchWrapper } from 'src/rtk/app/wrappers' + +export type StakeData = { + address: string + creatorSpaceId: string + stakeAmount: string + hasStaked: boolean +} + +const sliceName = 'stakes' + +export function getStakeId({ + address, + creatorSpaceId, +}: Pick) { + return `${address}-${creatorSpaceId}` +} + +const adapter = createEntityAdapter({ + selectId: data => getStakeId(data), +}) +const selectors = adapter.getSelectors(state => state.stakes) + +export const selectStakeForCreator = selectors.selectById + +export const fetchStakeData = createSimpleFetchWrapper< + { address: string; creatorSpaceId: string }, + StakeData +>({ + fetchData: async function ({ address, creatorSpaceId }) { + const data = await getStakeAmount({ address, spaceId: creatorSpaceId }) + const stakeAmount = data || { stakeAmount: '0', hasStaked: false } + const finalData = { address, creatorSpaceId, ...stakeAmount } + + return finalData + }, + saveToCacheAction: data => slice.actions.setStakeData(data), + getCachedData: (state, { address, creatorSpaceId }) => + selectStakeForCreator(state, getStakeId({ address, creatorSpaceId })), + sliceName, +}) + +const slice = createSlice({ + name: sliceName, + initialState: adapter.getInitialState(), + reducers: { + setStakeData: adapter.upsertOne, + }, +}) + +export default slice.reducer diff --git a/src/rtk/features/creators/totalStakeHooks.ts b/src/rtk/features/creators/totalStakeHooks.ts new file mode 100644 index 000000000..68d92cd5a --- /dev/null +++ b/src/rtk/features/creators/totalStakeHooks.ts @@ -0,0 +1,25 @@ +import { useMemo } from 'react' +import { useFetchWithoutApi } from 'src/rtk/app/hooksCommon' +import { useAppSelector } from 'src/rtk/app/store' +import { fetchTotalStake, selectTotalStake } from './totalStakeSlice' + +export function useTotalStake(address: string) { + return useAppSelector(state => selectTotalStake(state, address)) +} + +export function useFetchTotalStake(address: string) { + const data = useTotalStake(address) + + const args = useMemo(() => { + return { address } + }, [address]) + + const props = useFetchWithoutApi(fetchTotalStake, args, { + enabled: !!address, + }) + + return { + ...props, + data, + } +} diff --git a/src/rtk/features/creators/totalStakeSlice.ts b/src/rtk/features/creators/totalStakeSlice.ts new file mode 100644 index 000000000..07b36bd47 --- /dev/null +++ b/src/rtk/features/creators/totalStakeSlice.ts @@ -0,0 +1,43 @@ +import { createEntityAdapter, createSlice } from '@reduxjs/toolkit' +import { getTotalStake } from 'src/components/utils/OffchainUtils' +import { RootState } from 'src/rtk/app/rootReducer' +import { createSimpleFetchWrapper } from 'src/rtk/app/wrappers' + +export type TotalStake = { + address: string + amount: string + hasStaked?: boolean +} + +const sliceName = 'totalStakes' + +const adapter = createEntityAdapter({ + selectId: data => data.address, +}) +const selectors = adapter.getSelectors(state => state.totalStake) + +export const selectTotalStake = selectors.selectById + +export const fetchTotalStake = createSimpleFetchWrapper<{ address: string }, TotalStake>({ + sliceName, + fetchData: async function ({ address }: { address: string }) { + const data = await getTotalStake({ address }) + let stakeAmount = { amount: '0', hasStaked: false } + if (data) stakeAmount = data + const finalData = { address, ...stakeAmount } + + return finalData + }, + getCachedData: (state, { address }) => selectTotalStake(state, address), + saveToCacheAction: data => slice.actions.setTotalStake(data), +}) + +const slice = createSlice({ + name: sliceName, + initialState: adapter.getInitialState(), + reducers: { + setTotalStake: adapter.upsertOne, + }, +}) + +export default slice.reducer diff --git a/src/rtk/features/spaceIds/spaceIdsHooks.ts b/src/rtk/features/spaceIds/spaceIdsHooks.ts index 616be5efb..7572d29e1 100644 --- a/src/rtk/features/spaceIds/spaceIdsHooks.ts +++ b/src/rtk/features/spaceIds/spaceIdsHooks.ts @@ -73,3 +73,17 @@ export const useSelectSpaceIdsWhereAccountCanPost = (address?: AccountId) => return [...new Set([...ownSpaceIds, ...spaceIdsWithRolesByAccount])] }, shallowEqual) + +export const useSelectSpaceIdsWhereAccountCanPostWithLoadingStatus = (address?: AccountId) => + useAppSelector(state => { + if (!address) return { isLoading: false, spaceIds: [] } + + const ownSpaceIds = selectEntityOfSpaceIdsByOwner(state, { id: address }) + const isLoading = !ownSpaceIds + const spaceIdsWithRolesByAccount = selectSpaceIdsWithRolesByAccount(state, address) || [] + + return { + isLoading, + spaceIds: [...new Set([...(ownSpaceIds?.ownSpaceIds ?? []), ...spaceIdsWithRolesByAccount])], + } + }, shallowEqual) diff --git a/src/styles/antd.css b/src/styles/antd.css index f4597c892..164af572b 100644 --- a/src/styles/antd.css +++ b/src/styles/antd.css @@ -22563,7 +22563,7 @@ textarea.ant-input-number { } .ant-layout.ant-layout-has-sider > .ant-layout, .ant-layout.ant-layout-has-sider > .ant-layout-content { - overflow-x: hidden; + overflow-x: clip; } .ant-layout-header, .ant-layout-footer { diff --git a/src/styles/subsocial-vars.scss b/src/styles/subsocial-vars.scss index a5cbf234f..cf7c7a570 100644 --- a/src/styles/subsocial-vars.scss +++ b/src/styles/subsocial-vars.scss @@ -3,6 +3,7 @@ $font_tiny: 0.75rem; $font_small: 0.875rem; $font_normal: 1rem; +$font_semilarge: 1.125rem; $font_large: 1.25rem; $font_big: 1.5rem; $font_huge: 2rem; diff --git a/src/styles/subsocial.scss b/src/styles/subsocial.scss index 0dd9a16f3..c119087eb 100644 --- a/src/styles/subsocial.scss +++ b/src/styles/subsocial.scss @@ -120,6 +120,10 @@ a { font-size: $font_normal !important; } +.FontSemilarge { + font-size: $font_semilarge; +} + .FontLarge { font-size: $font_large; } @@ -128,6 +132,16 @@ a { font-size: $font_big; } +.ColorPrimary { + color: $color_primary; +} +.ColorMuted { + color: $color_muted; +} +.ColorWhite { + color: white; +} + .GapMini { gap: $space_mini; } @@ -199,6 +213,16 @@ a { border-radius: $border_radius_huge; } +.FontWeightMedium { + font-weight: $font_weight_medium; +} +.FontWeightSemibold { + font-weight: $font_weight_semibold; +} +.FontWeightBold { + font-weight: $font_weight_bold; +} + .DfImagePreview { width: 100%; } @@ -638,6 +662,15 @@ a { } } +.HideScrollbar { + /* Hide scrollbar for Chrome, Safari and Opera */ + &::-webkit-scrollbar { + display: none; + } + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + .ant-dropdown-trigger { height: min-content; } @@ -880,17 +913,15 @@ hr { width: 100%; display: flex; align-items: center; - justify-content: space-between; + gap: $space_big; - &.DfActionBorder { - border-top: 1px solid rgba(34, 36, 38, 0.15); + > * { + padding: 0; + height: auto; } - .DfReactionsAction { - button:first-of-type { - // margin-left: -$space_normal; - margin-right: $space_normal; - } + &.DfActionBorder { + border-top: 1px solid rgba(34, 36, 38, 0.15); } } @@ -919,7 +950,7 @@ hr { } .DfSegment.DfPostPreview { - padding-bottom: 0; + padding-bottom: $space_normal; .DfInfo { width: -webkit-fill-available; @@ -939,11 +970,6 @@ hr { border: 1px solid $color_light_border; } } - - .DfActionsPanel { - padding: 0.25rem 0; - justify-content: space-evenly; - } } .DfPostImagePreviewWrapper { @@ -1091,7 +1117,7 @@ hr { display: flex; justify-content: center; align-items: flex-start; - gap: $space_big; + gap: $space_normal; } @media (min-width: 767px) { @@ -1100,6 +1126,8 @@ hr { max-width: $max_width_content; width: $max_width_content; padding-bottom: 0; + position: relative; + z-index: 0; .DfSectionOuter { width: 100%; @@ -1236,6 +1264,10 @@ hr { padding: 0 $space_huge; } +.ant-btn { + font-weight: $font_weight_medium; +} + .DfSearch { width: 450px; display: flex; diff --git a/src/utils/links.ts b/src/utils/links.ts index 0687cd896..10ff1a57b 100644 --- a/src/utils/links.ts +++ b/src/utils/links.ts @@ -1 +1,19 @@ +import { SpaceData } from '@subsocial/api/types' +import { getCurrentWallet } from 'src/components/auth/utils' +import { getSpaceHandleOrId } from './spaces' + export const getSubsocialDiscordLink = () => 'https://discord.com/invite/w2Rqy2M' + +export const getSubIdCreatorsLink = (space?: SpaceData) => + `https://sub.id/creators/${space ? getSpaceHandleOrId(space.struct) : ''}` + +export const activeStakingLinks = { + learnMore: + 'https://polkaverse.com/@subsocial/boost-staking-rewards-by-up-to-3x-in-the-active-staking-40404', + discuss: () => { + const currentWallet = getCurrentWallet() + const link = 'https://grill.chat/creators/stakers-20132' + if (!currentWallet) return link + return `${link}?wallet=${currentWallet}` + }, +} diff --git a/src/utils/spaces.ts b/src/utils/spaces.ts new file mode 100644 index 000000000..e1eaac4ff --- /dev/null +++ b/src/utils/spaces.ts @@ -0,0 +1,8 @@ +import { SpaceStruct } from '@subsocial/api/types' + +export function getSpaceHandleOrId(spaceStruct?: SpaceStruct) { + let handleOrId = spaceStruct?.handle + if (handleOrId) handleOrId = `@${handleOrId}` + + return handleOrId || spaceStruct?.id +} diff --git a/yarn.lock b/yarn.lock index 00718aade..c403f9993 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2395,15 +2395,15 @@ "@elastic/elasticsearch" "7.4.0" "@subsocial/utils" latest -"@subsocial/grill-widget@^0.0.12": - version "0.0.12" - resolved "https://registry.yarnpkg.com/@subsocial/grill-widget/-/grill-widget-0.0.12.tgz#57384850a3adb5342a5505a3f0d67c258d7e3b0e" - integrity sha512-Xed+wN/NEZ+mQlKA1olVVcBviVIQOTyqaCYcl9pGsg8yDdIBsKVjqJUKty1vvBL7gY0A+1osdwBgX2xUsbHOBA== +"@subsocial/grill-widget@^0.0.13": + version "0.0.13" + resolved "https://registry.yarnpkg.com/@subsocial/grill-widget/-/grill-widget-0.0.13.tgz#b3001beb2c5eef140f36befff00a581892f7547f" + integrity sha512-16t1zgWC0nFP/iqhlYEVkRL+kzSqBfjTS2xhCPMNTzb51MjpVpGgZLTd77Jq3NU/iqnplyH690iKGzQIixHFkw== -"@subsocial/resource-discussions@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@subsocial/resource-discussions/-/resource-discussions-0.0.3.tgz#18a13c8ad27b039328e8fc8ac330b7f923a271cc" - integrity sha512-XCrDFbypAyTibLgvEPYJkqI1uDoX+wmnyqis8Hrher6Y9QFGiG7gPnNk/Bmv5zmiEiz9HJ5k+C6IwbiS5B+KyQ== +"@subsocial/resource-discussions@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@subsocial/resource-discussions/-/resource-discussions-0.0.4.tgz#4afeb53dde4accc0c70e80fc245b1416c74afdf2" + integrity sha512-GAuC6SFfyrSpCdIuwLlA/hOouNgcj+6j+kZiDPz/cxLcNPdB4gqGVNnqKaL/rNE3MLNetpgI+gmatTomGRKeiA== dependencies: graphology "^0.25.1" @@ -2862,6 +2862,13 @@ dependencies: "@types/lodash" "*" +"@types/lodash.shuffle@^4.2.9": + version "4.2.9" + resolved "https://registry.yarnpkg.com/@types/lodash.shuffle/-/lodash.shuffle-4.2.9.tgz#4af1b1c98dd8be8c0c6387e6a62caa476d0594a9" + integrity sha512-4siLZ4/vQH4T7Bm4254sG4n6hh9k7vd/bqfDVoeIwSha4Itu3MuoTxPX2I2Tue2JN94y7Y2I27QzwHZLdMlrBg== + dependencies: + "@types/lodash" "*" + "@types/lodash.truncate@^4.4.6": version "4.4.7" resolved "https://registry.yarnpkg.com/@types/lodash.truncate/-/lodash.truncate-4.4.7.tgz#662a66e990b2de7002400d960c377cae95d731a3" @@ -8923,7 +8930,7 @@ kind-of@^5.0.0: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== -kind-of@^6.0.0, kind-of@^6.0.2: +kind-of@^6.0.0, kind-of@^6.0.2, kind-of@~6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== @@ -9199,6 +9206,11 @@ lodash.pickby@4.6.0: resolved "https://registry.yarnpkg.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz#7dea21d8c18d7703a27c704c15d3b84a67e33aff" integrity sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q== +lodash.shuffle@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.shuffle/-/lodash.shuffle-4.2.0.tgz#145b5053cf875f6f5c2a33f48b6e9948c6ec7b4b" + integrity sha512-V/rTAABKLFjoecTZjKSv+A1ZomG8hZg8hlgeG6wwQVD9AGv+10zqqSf6mFq2tVA703Zd5R0YhSuSlXA+E/Ei+Q== + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -12640,6 +12652,21 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +sort-keys-recursive@^2.1.10: + version "2.1.10" + resolved "https://registry.yarnpkg.com/sort-keys-recursive/-/sort-keys-recursive-2.1.10.tgz#df5e22d3f3ff0427fdc4a088f16c37c1839456b8" + integrity sha512-yRLJbEER/PjU7hSRwXvP+NyXiORufu8rbSbp+3wFRuJZXoi/AhuKczbjuipqn7Le0SsTXK4VUeri2+Ni6WS8Hg== + dependencies: + kind-of "~6.0.2" + sort-keys "~4.2.0" + +sort-keys@~4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-4.2.0.tgz#6b7638cee42c506fff8c1cecde7376d21315be18" + integrity sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg== + dependencies: + is-plain-obj "^2.0.0" + "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"