diff --git a/README.md b/README.md index e52cc36eb8..f4ac134be3 100644 --- a/README.md +++ b/README.md @@ -74,3 +74,5 @@ Hit up a member of the core team! - `moon run web:typecheck` for checking type validity - `moon run web:synpress-run` to run e2e tests - `moon run web:synpress-open` to open cypress +- `yarn fetch-schema` to pull graphql schema from production +- `yarn fetch-schema-dev` to pull graphql schema from development diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 574c5c48fa..3a9c664171 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -33,7 +33,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ orientation: 'portrait', icon: './assets/icon.png', userInterfaceStyle: 'automatic', - version: '1.0.26', + version: '1.0.28', updates: { fallbackToCacheTimeout: 0, }, diff --git a/apps/mobile/src/components/AnimatedRefreshIcon.tsx b/apps/mobile/src/components/AnimatedRefreshIcon.tsx new file mode 100644 index 0000000000..9849734878 --- /dev/null +++ b/apps/mobile/src/components/AnimatedRefreshIcon.tsx @@ -0,0 +1,80 @@ +import { useCallback, useEffect, useRef } from 'react'; +import { Animated } from 'react-native'; +import { RefreshIcon } from 'src/icons/RefreshIcon'; + +import { IconContainer } from '~/components/IconContainer'; +import { useToastActions } from '~/contexts/ToastContext'; +import { contexts } from '~/shared/analytics/constants'; + +type AnimatedRefreshIconProps = { + onSync: () => void; + onRefresh: () => void; + isSyncing: boolean; + eventElementId: string; + eventName: string; +}; + +export function AnimatedRefreshIcon({ + onSync, + onRefresh, + isSyncing, + eventElementId, + eventName, +}: AnimatedRefreshIconProps) { + const { pushToast } = useToastActions(); + + const handleSync = useCallback(async () => { + if (isSyncing) return; + + await onSync(); + onRefresh(); + pushToast({ + message: 'Successfully refreshed your collection', + withoutNavbar: true, + }); + }, [isSyncing, onSync, onRefresh, pushToast]); + + const spinValue = useRef(new Animated.Value(0)).current; + + const spin = useCallback(() => { + spinValue.setValue(0); + Animated.timing(spinValue, { + toValue: 1, + duration: 1000, + useNativeDriver: true, + }).start(({ finished }) => { + // Only repeat the animation if it completed (wasn't interrupted) and isSyncing is still true + if (finished && isSyncing) { + spin(); + } + }); + }, [isSyncing, spinValue]); + + const spinAnimation = spinValue.interpolate({ + inputRange: [0, 1], + outputRange: ['0deg', '360deg'], + }); + + useEffect(() => { + if (isSyncing) { + spin(); + } else { + spinValue.stopAnimation(); + } + }, [isSyncing, spin, spinValue]); + + return ( + + + + } + eventElementId={eventElementId} + eventName={eventName} + eventContext={contexts.Posts} + /> + ); +} diff --git a/apps/mobile/src/components/BackButton.tsx b/apps/mobile/src/components/BackButton.tsx index 83d0c1e232..506b69e3dc 100644 --- a/apps/mobile/src/components/BackButton.tsx +++ b/apps/mobile/src/components/BackButton.tsx @@ -19,6 +19,7 @@ export function BackButton({ onPress, size = 'md' }: Props) { size={size} eventElementId={null} eventName={null} + eventContext={null} icon={} onPress={handlePress} /> diff --git a/apps/mobile/src/components/BottomSheetRow.tsx b/apps/mobile/src/components/BottomSheetRow.tsx new file mode 100644 index 0000000000..eb6cfc8038 --- /dev/null +++ b/apps/mobile/src/components/BottomSheetRow.tsx @@ -0,0 +1,54 @@ +import clsx from 'clsx'; +import { View } from 'react-native'; + +import { GalleryElementTrackingProps } from '~/shared/contexts/AnalyticsContext'; + +import { GalleryTouchableOpacity } from './GalleryTouchableOpacity'; +import { Typography } from './Typography'; + +type BottomSheetRowProps = { + icon?: React.ReactNode; + text: string; + onPress: () => void; + style?: React.ComponentProps['style']; + isConfirmationRow?: boolean; + fontWeight?: 'Regular' | 'Bold'; + rightIcon?: React.ReactNode; + eventContext: GalleryElementTrackingProps['eventContext']; +}; + +export function BottomSheetRow({ + icon, + text, + onPress, + style, + isConfirmationRow, + fontWeight = 'Regular', + rightIcon, + eventContext, +}: BottomSheetRowProps) { + return ( + + + {icon && {icon}} + + {text} + + {rightIcon && {rightIcon}} + + + ); +} diff --git a/apps/mobile/src/components/Boundaries/TokenFailureBoundary/TokenFailureFallbacks.tsx b/apps/mobile/src/components/Boundaries/TokenFailureBoundary/TokenFailureFallbacks.tsx index 4fb48566a9..aab964126c 100644 --- a/apps/mobile/src/components/Boundaries/TokenFailureBoundary/TokenFailureFallbacks.tsx +++ b/apps/mobile/src/components/Boundaries/TokenFailureBoundary/TokenFailureFallbacks.tsx @@ -8,6 +8,7 @@ import { GalleryTouchableOpacity } from '~/components/GalleryTouchableOpacity'; import { useTokenStateManagerContext } from '~/contexts/TokenStateManagerContext'; import { TokenFailureFallbacksErrorFallbackFragment$key } from '~/generated/TokenFailureFallbacksErrorFallbackFragment.graphql'; import { TokenFailureFallbacksLoadingFallbackFragment$key } from '~/generated/TokenFailureFallbacksLoadingFallbackFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; export type FallbackProps = { fallbackAspectSquare?: boolean; @@ -80,6 +81,7 @@ export function TokenPreviewErrorFallback({ onPress={handlePress} eventElementId="Refresh Broken Token Button" eventName="Refresh Broken Token Pressed" + eventContext={contexts.Error} activeOpacity={refreshable ? 0.2 : 1} > setActive(true)} onPressOut={() => setActive(false)} + // TODO: analytics this should be prop drilled eventElementId="Follow Button" eventName="Follow Button Clicked" + eventContext={null} activeOpacity={1} properties={{ variant, ...eventProperties }} onPress={onPress} diff --git a/apps/mobile/src/components/CommunitiesList/CommunityCard.tsx b/apps/mobile/src/components/CommunitiesList/CommunityCard.tsx index 21ab0e9429..97247717e3 100644 --- a/apps/mobile/src/components/CommunitiesList/CommunityCard.tsx +++ b/apps/mobile/src/components/CommunitiesList/CommunityCard.tsx @@ -5,6 +5,7 @@ import { graphql } from 'relay-runtime'; import { Typography } from '~/components/Typography'; import { CommunityCardFragment$key } from '~/generated/CommunityCardFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; import { GalleryTouchableOpacity } from '../GalleryTouchableOpacity'; import { Markdown } from '../Markdown'; @@ -48,6 +49,7 @@ export function CommunityCard({ communityRef, onPress }: CommunityCardProps) { className="flex flex-row items-center space-x-4 py-2 px-4" eventElementId="Community Name" eventName="Community Name Clicked" + eventContext={contexts.Community} > diff --git a/apps/mobile/src/components/Community/CommunityHeader.tsx b/apps/mobile/src/components/Community/CommunityHeader.tsx index a9a21754c1..938e8c242a 100644 --- a/apps/mobile/src/components/Community/CommunityHeader.tsx +++ b/apps/mobile/src/components/Community/CommunityHeader.tsx @@ -3,6 +3,7 @@ import { View } from 'react-native'; import { graphql, useFragment } from 'react-relay'; import { CommunityHeaderFragment$key } from '~/generated/CommunityHeaderFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; import { truncateAddress } from '~/shared/utils/wallet'; import { GalleryBottomSheetModalType } from '../GalleryBottomSheet/GalleryBottomSheetModal'; @@ -53,8 +54,9 @@ export function CommunityHeader({ communityRef }: Props) { diff --git a/apps/mobile/src/components/Community/CommunityMeta.tsx b/apps/mobile/src/components/Community/CommunityMeta.tsx index 69c7fba02f..16cf874404 100644 --- a/apps/mobile/src/components/Community/CommunityMeta.tsx +++ b/apps/mobile/src/components/Community/CommunityMeta.tsx @@ -18,6 +18,7 @@ import { CommunityMetaQueryFragment$key } from '~/generated/CommunityMetaQueryFr import { CommunityMetaRefetchQuery } from '~/generated/CommunityMetaRefetchQuery.graphql'; import { PostIcon } from '~/navigation/MainTabNavigator/PostIcon'; import { MainTabStackNavigatorProp } from '~/navigation/types'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import { Button } from '../Button'; @@ -160,6 +161,7 @@ export function CommunityMeta({ communityRef, queryRef }: Props) { onPress={handleUsernamePress} eventElementId="Community Page Creator Username" eventName="Tapped Community Page Creator Username" + eventContext={contexts.Community} > {community.creator.__typename && } @@ -174,12 +176,20 @@ export function CommunityMeta({ communityRef, queryRef }: Props) { } else if (community.contractAddress) { return ( - + ); @@ -228,8 +238,9 @@ export function CommunityMeta({ communityRef, queryRef }: Props) { variant={isMemberOfCommunity ? 'primary' : 'disabled'} icon={} onPress={handlePress} - eventElementId={null} - eventName={null} + eventElementId="Attempt Create Post Button" + eventName="Attempt Create Post" + eventContext={contexts.Community} /> (null); @@ -105,15 +106,22 @@ function CommunityPostBottomSheet( refreshing your collection. - - - - + + + - - - - + - - - - - -
- ButtonLink - - primary - - primary - - - primary - - - primary - - - - - secondary - - - secondary - - - secondary - - - secondary - + +
@@ -180,7 +183,14 @@ export default function DesignPage() {
NFT Selector - +
diff --git a/apps/web/pages/features/posts.tsx b/apps/web/pages/features/posts.tsx new file mode 100644 index 0000000000..0f08281dde --- /dev/null +++ b/apps/web/pages/features/posts.tsx @@ -0,0 +1,58 @@ +import GalleryRoute from '~/scenes/_Router/GalleryRoute'; +import { CmsTypes } from '~/scenes/ContentPages/cms_types'; +import PostsFeaturePage from '~/scenes/ContentPages/PostsFeaturePage'; +import { fetchSanityContent } from '~/utils/sanity'; + +type Props = { + pageContent: CmsTypes.FeaturePage; +}; + +export default function PostsFeatureRoute({ pageContent }: Props) { + return } navbar={false} />; +} + +export const featurePostsPageContentQuery = ` +*[ _type == "featurePage" && id == "posts" ]{ + ..., + "featureHighlights": featureHighlights[]->{ + heading, + orientation, + body, + externalLink, + media{ + mediaType, + image{ + asset->{ + url + }, + alt + }, + video{ + asset->{ + url + } + } + } + }, + "faqModule": faqModule->{ + title, + faqs + }, + "splashImage": { + "asset": splashImage.asset->{ + url + }, + alt + } +} | order(date desc) +`; + +export const getServerSideProps = async () => { + const content = await fetchSanityContent(featurePostsPageContentQuery); + + return { + props: { + pageContent: content[0], + }, + }; +}; diff --git a/apps/web/pages/onboarding/add-email.tsx b/apps/web/pages/onboarding/add-email.tsx index 12f43dc651..820d2088c3 100644 --- a/apps/web/pages/onboarding/add-email.tsx +++ b/apps/web/pages/onboarding/add-email.tsx @@ -63,10 +63,10 @@ export default function AddEmail() { push(userProfileRoute); }, [push, track, userProfileRoute]); - const handleSkip = useCallback(() => { - track('Onboarding: add-email Skip click'); - push(userProfileRoute); - }, [push, track, userProfileRoute]); + // const handleSkip = useCallback(() => { + // track('Onboarding: add-email Skip click'); + // push(userProfileRoute); + // }, [push, track, userProfileRoute]); return ( @@ -88,9 +88,10 @@ export default function AddEmail() { ); diff --git a/apps/web/public/.well-known/apple-app-site-association b/apps/web/public/.well-known/apple-app-site-association index 54ee43325d..dc331275ee 100644 --- a/apps/web/public/.well-known/apple-app-site-association +++ b/apps/web/public/.well-known/apple-app-site-association @@ -26,6 +26,7 @@ "NOT /explore", "NOT /faq", "NOT /featured", + "NOT /features/*", "NOT /feed", "NOT /feeds", "NOT /gallery", @@ -39,6 +40,7 @@ "NOT /members", "NOT /membership", "NOT /messages", + "NOT /mint/*", "NOT /nft", "NOT /nfts", "NOT /notifications", diff --git a/apps/web/public/1k-posts-memento-min.jpg b/apps/web/public/1k-posts-memento-min.jpg new file mode 100644 index 0000000000..c75757900a Binary files /dev/null and b/apps/web/public/1k-posts-memento-min.jpg differ diff --git a/apps/web/public/base-gallery-memento.jpg b/apps/web/public/base-gallery-memento.jpg deleted file mode 100644 index 0a5933c30e..0000000000 Binary files a/apps/web/public/base-gallery-memento.jpg and /dev/null differ diff --git a/apps/web/src/abis/galleryMementosContractAbi.ts b/apps/web/src/abis/galleryMementosContractAbi.ts index b051b154c0..310ff5661f 100644 --- a/apps/web/src/abis/galleryMementosContractAbi.ts +++ b/apps/web/src/abis/galleryMementosContractAbi.ts @@ -16,6 +16,11 @@ export const GALLERY_MEMENTOS_CONTRACT_ABI = [ name: 'to', type: 'address', }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, { internalType: 'bytes32[]', name: 'merkleProof', diff --git a/apps/web/src/components/Announcement/AnnouncementList.tsx b/apps/web/src/components/Announcement/AnnouncementList.tsx index 1b21942065..25a096f241 100644 --- a/apps/web/src/components/Announcement/AnnouncementList.tsx +++ b/apps/web/src/components/Announcement/AnnouncementList.tsx @@ -6,15 +6,16 @@ import styled from 'styled-components'; import { useDrawerActions } from '~/contexts/globalLayout/GlobalSidebar/SidebarDrawerContext'; import { AnnouncementListFragment$key } from '~/generated/AnnouncementListFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import colors from '~/shared/theme/colors'; import { HTTPS_URL } from '~/shared/utils/regex'; import { useOptimisticallyDismissExperience } from '~/utils/graphql/experiences/useUpdateUserExperience'; -import { Chip } from '../core/Chip/Chip'; +import { GalleryChip } from '../core/Chip/Chip'; import { HStack, VStack } from '../core/Spacer/Stack'; import { BaseM, BaseS } from '../core/Text/Text'; -import useAnnouncement, { AnnouncementType } from './useAnnouncement'; +import useAnnouncement, { DecoratedAnnouncementType } from './useAnnouncement'; type Props = { queryRef: AnnouncementListFragment$key; @@ -38,7 +39,7 @@ export default function AnnouncementList({ queryRef }: Props) { const { hideDrawer } = useDrawerActions(); const handleClick = useCallback( - (announcement: AnnouncementType) => { + (announcement: DecoratedAnnouncementType) => { track('Announcement click', { type: announcement.key }); // if there is a link, open it @@ -97,7 +98,13 @@ export default function AnnouncementList({ queryRef }: Props) { {announcement.ctaText && ( - Download + + {announcement.ctaText} + )} @@ -134,7 +141,7 @@ const StyledAnnouncementDescriptionContainer = styled(VStack)` const StyledCTAContainer = styled(HStack)``; -const StyledChip = styled(Chip)` +const StyledChip = styled(GalleryChip)` background-color: ${colors.black['800']}; color: ${colors.offWhite}; width: 88px; diff --git a/apps/web/src/components/Announcement/constants.ts b/apps/web/src/components/Announcement/constants.ts index b048f4a844..ffa7b8eb48 100644 --- a/apps/web/src/components/Announcement/constants.ts +++ b/apps/web/src/components/Announcement/constants.ts @@ -1,15 +1,23 @@ // key: based on `UserExperienceType` schema -export const ANNOUNCEMENT_CONTENT = [ - // Enable this in the future - // { - // key: 'UpsellMintMemento5', - // title: 'Now Minting: Worlds Beyond', - // description: - // 'Gallery Memento #5 is now available for minting, an emblem of the transformational steps Gallery is taking into the exciting world of mobile.', - // date: '2023-06-23T15:00:00.154845Z', - // link: '/mint/mementos', - // }, +import { UserExperienceType } from '~/generated/enums'; +import { GalleryElementTrackingProps } from '~/shared/contexts/AnalyticsContext'; + +export type AnnouncementType = { + key: UserExperienceType; + title: string; + description: string; + date: string; + eventElementId: GalleryElementTrackingProps['eventElementId']; + eventName: GalleryElementTrackingProps['eventName']; + link: string; + ctaText?: string; +}; + +export const ANNOUNCEMENT_CONTENT: AnnouncementType[] = [ + // TODO: for future notifications, pass in `eventElementId` and `eventName`. + // we don't need to do this for older events because we don't display announcements + // that are older than 30 days { key: 'MobileBetaUpsell', title: 'The Wait is Over!', @@ -18,6 +26,8 @@ export const ANNOUNCEMENT_CONTENT = [ date: '2023-06-20T16:00:00.154845Z', link: '/mobile', ctaText: 'Download', + eventElementId: null, + eventName: null, }, // older notifications { @@ -27,6 +37,8 @@ export const ANNOUNCEMENT_CONTENT = [ 'The waitlist for the Gallery mobile app is now open. Claim your spot now and be among the first to experience effortless browsing and the magic of the creative web in your pocket.', date: '2023-05-08T16:00:00.154845Z', link: '/mobile', + eventElementId: null, + eventName: null, }, { key: 'UpsellGallerySelects1', @@ -35,6 +47,8 @@ export const ANNOUNCEMENT_CONTENT = [ '🌸 Submit a gallery in the vibrant spirit of Spring for a chance to win an exclusive 1-of-1 NFT and merch bundle.', date: '2023-04-10T13:00:00.154845Z', link: 'https://gallery.mirror.xyz/GzEODA-g4mvdb1onS1jSRMSKqfMoGJCNu5yOSTV9RM8', + eventElementId: null, + eventName: null, }, { key: 'UpsellMintMemento4', @@ -43,5 +57,7 @@ export const ANNOUNCEMENT_CONTENT = [ 'Gallery Memento #4 is now available for minting, a beautiful and symbolic representation of the growing network within the Gallery community.', date: '2023-03-27T13:00:00.154845Z', link: '/mint/mementos', + eventElementId: null, + eventName: null, }, ]; diff --git a/apps/web/src/components/Announcement/useAnnouncement.tsx b/apps/web/src/components/Announcement/useAnnouncement.tsx index cb81882558..7ce474f856 100644 --- a/apps/web/src/components/Announcement/useAnnouncement.tsx +++ b/apps/web/src/components/Announcement/useAnnouncement.tsx @@ -1,24 +1,16 @@ import { useMemo } from 'react'; import { graphql, useFragment } from 'react-relay'; -import { - useAnnouncementFragment$key, - UserExperienceType, -} from '~/generated/useAnnouncementFragment.graphql'; +import { UserExperienceType } from '~/generated/enums'; +import { useAnnouncementFragment$key } from '~/generated/useAnnouncementFragment.graphql'; import { getDaysSince, getTimeSince } from '~/shared/utils/time'; -import { ANNOUNCEMENT_CONTENT } from './constants'; +import { ANNOUNCEMENT_CONTENT, AnnouncementType } from './constants'; -export type AnnouncementType = { - key: UserExperienceType; - title: string; - description: string; - date: string; +export type DecoratedAnnouncementType = { time: string | null; // time since date experienced: boolean; - link?: string; - ctaText?: string; -}; +} & AnnouncementType; export default function useAnnouncement(queryRef: useAnnouncementFragment$key) { const query = useFragment( @@ -37,7 +29,7 @@ export default function useAnnouncement(queryRef: useAnnouncementFragment$key) { queryRef ); - const announcements = useMemo(() => { + const announcements = useMemo(() => { const userExperiences = query.viewer?.userExperiences ?? []; const announcementsLists = ANNOUNCEMENT_CONTENT; diff --git a/apps/web/src/components/Badge/Badge.tsx b/apps/web/src/components/Badge/Badge.tsx index 5d9e31e167..49a256e9d3 100644 --- a/apps/web/src/components/Badge/Badge.tsx +++ b/apps/web/src/components/Badge/Badge.tsx @@ -3,18 +3,20 @@ import { useCallback, useMemo, useState } from 'react'; import { graphql, useFragment } from 'react-relay'; import styled from 'styled-components'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import IconContainer from '~/components/core/IconContainer'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; import Tooltip from '~/components/Tooltip/Tooltip'; import { BADGE_ENABLED_COMMUNITY_ADDRESSES } from '~/constants/community'; import { BadgeFragment$key } from '~/generated/BadgeFragment.graphql'; +import { GalleryElementTrackingProps } from '~/shared/contexts/AnalyticsContext'; import { LowercaseChain } from '~/shared/utils/chains'; type Props = { badgeRef: BadgeFragment$key; + eventContext: GalleryElementTrackingProps['eventContext']; }; -export default function Badge({ badgeRef }: Props) { +export default function Badge({ badgeRef, eventContext }: Props) { const [showTooltip, setShowTooltip] = useState(false); const badge = useFragment( @@ -63,7 +65,12 @@ export default function Badge({ badgeRef }: Props) { } return ( - + } /> - + ); } @@ -85,7 +92,7 @@ const StyledTooltip = styled(Tooltip)<{ showTooltip: boolean }>` transform: translateY(${({ showTooltip }) => (showTooltip ? -28 : -24)}px); `; -const StyledInteractiveLink = styled(InteractiveLink)` +const StyledGalleryLink = styled(GalleryLink)` position: relative; line-height: 1; outline: none; diff --git a/apps/web/src/components/CommunityHolderGrid/CommunityHolderGridItem.tsx b/apps/web/src/components/CommunityHolderGrid/CommunityHolderGridItem.tsx index 72f578a98f..2a9a9b0ef1 100644 --- a/apps/web/src/components/CommunityHolderGrid/CommunityHolderGridItem.tsx +++ b/apps/web/src/components/CommunityHolderGrid/CommunityHolderGridItem.tsx @@ -3,13 +3,14 @@ import { graphql, useFragment } from 'react-relay'; import styled from 'styled-components'; import breakpoints from '~/components/core/breakpoints'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { VStack } from '~/components/core/Spacer/Stack'; import { BaseM } from '~/components/core/Text/Text'; import { useModalActions } from '~/contexts/modal/ModalContext'; import { CommunityHolderGridItemFragment$key } from '~/generated/CommunityHolderGridItemFragment.graphql'; import { CommunityHolderGridItemQueryFragment$key } from '~/generated/CommunityHolderGridItemQueryFragment.graphql'; import TokenDetailView from '~/scenes/TokenDetailPage/TokenDetailView'; +import { contexts } from '~/shared/analytics/constants'; import { useGetSinglePreviewImage } from '~/shared/relay/useGetPreviewImages'; import { extractRelevantMetadataFromToken } from '~/shared/utils/extractRelevantMetadataFromToken'; import { graphqlTruncateUniversalUsername } from '~/shared/utils/wallet'; @@ -81,13 +82,18 @@ export default function CommunityHolderGridItem({ holderRef, queryRef }: Props) return ( - + - + {token?.name} {owner?.universal ? ( - {usernameWithFallback} + {usernameWithFallback} ) : ( )} @@ -111,6 +117,6 @@ const StyledNftDetailViewPopover = styled(VStack)` } `; -const StyledInteractiveLink = styled(InteractiveLink)` +const StyledGalleryLink = styled(GalleryLink)` text-decoration: none; `; diff --git a/apps/web/src/components/Debugger/Debugger.tsx b/apps/web/src/components/Debugger/Debugger.tsx index 56b02d4ad2..739c7ab223 100644 --- a/apps/web/src/components/Debugger/Debugger.tsx +++ b/apps/web/src/components/Debugger/Debugger.tsx @@ -126,7 +126,14 @@ const Debugger = () => { type="password" /> )} - + + Submit diff --git a/apps/web/src/components/Email/EmailForm.tsx b/apps/web/src/components/Email/EmailForm.tsx index 457392fafa..72a1f2c4ee 100644 --- a/apps/web/src/components/Email/EmailForm.tsx +++ b/apps/web/src/components/Email/EmailForm.tsx @@ -7,6 +7,7 @@ import styled from 'styled-components'; import { useToastActions } from '~/contexts/toast/ToastContext'; import { EmailFormFragment$key } from '~/generated/EmailFormFragment.graphql'; import { EmailFormMutation } from '~/generated/EmailFormMutation.graphql'; +import { contexts } from '~/shared/analytics/constants'; import { AdditionalContext, useReportError } from '~/shared/contexts/ErrorReportingContext'; import useDebounce from '~/shared/hooks/useDebounce'; import { usePromisifiedMutation } from '~/shared/relay/usePromisifiedMutation'; @@ -225,11 +226,21 @@ function EmailForm({ setIsEditMode, queryRef, onClose }: Props) { {showCancelButton && ( - )} diff --git a/apps/web/src/components/Feed/Posts/DeletePostConfirmation.tsx b/apps/web/src/components/Feed/Posts/DeletePostConfirmation.tsx index 70732eac7c..ddf8a2121e 100644 --- a/apps/web/src/components/Feed/Posts/DeletePostConfirmation.tsx +++ b/apps/web/src/components/Feed/Posts/DeletePostConfirmation.tsx @@ -6,6 +6,7 @@ import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { BaseM } from '~/components/core/Text/Text'; import { useModalActions } from '~/contexts/modal/ModalContext'; import useDeletePost from '~/hooks/api/posts/useDeletePost'; +import { contexts } from '~/shared/analytics/constants'; type Props = { postDbid: string; @@ -34,7 +35,14 @@ export default function DeletePostConfirmation({ postDbid, communityId }: Props) This cannot be undone. - + Delete diff --git a/apps/web/src/components/Feed/Posts/PostCommunityPill.tsx b/apps/web/src/components/Feed/Posts/PostCommunityPill.tsx index bd608f36d6..ce2329514f 100644 --- a/apps/web/src/components/Feed/Posts/PostCommunityPill.tsx +++ b/apps/web/src/components/Feed/Posts/PostCommunityPill.tsx @@ -3,11 +3,11 @@ import { graphql, useFragment } from 'react-relay'; import styled from 'styled-components'; import { TitleDiatypeM } from '~/components/core/Text/Text'; +import { GalleryPill } from '~/components/GalleryPill'; import CommunityHoverCard from '~/components/HoverCard/CommunityHoverCard'; -import { ButtonPill } from '~/components/Pill'; import { PostCommunityPillFragment$key } from '~/generated/PostCommunityPillFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; -import colors from '~/shared/theme/colors'; import { extractRelevantMetadataFromToken } from '~/shared/utils/extractRelevantMetadataFromToken'; type Props = { @@ -53,23 +53,19 @@ export default function PostCommunityPill({ postRef }: Props) { communityName={contractName} onClick={handleClick} > - + {contractName} ); } -const StyledPill = styled(ButtonPill)` - background-color: ${colors.white}; - color: ${colors.black['800']}; +const StyledPill = styled(GalleryPill)` height: 28px; - padding: 4px 12px; - - &:hover { - border-color: ${colors.black['800']}; - background-color: ${colors.white}; - } `; const StyledCommunityName = styled(TitleDiatypeM)` diff --git a/apps/web/src/components/Feed/Posts/PostDropdown.tsx b/apps/web/src/components/Feed/Posts/PostDropdown.tsx index ac9896e323..ea5e3ec91e 100644 --- a/apps/web/src/components/Feed/Posts/PostDropdown.tsx +++ b/apps/web/src/components/Feed/Posts/PostDropdown.tsx @@ -5,12 +5,12 @@ import CopyToClipboard from '~/components/CopyToClipboard/CopyToClipboard'; import { DropdownItem } from '~/components/core/Dropdown/DropdownItem'; import { DropdownSection } from '~/components/core/Dropdown/DropdownSection'; import SettingsDropdown from '~/components/core/Dropdown/SettingsDropdown'; -import { BaseM } from '~/components/core/Text/Text'; import { useModalActions } from '~/contexts/modal/ModalContext'; import { PostDropdownFragment$key } from '~/generated/PostDropdownFragment.graphql'; import { PostDropdownQueryFragment$key } from '~/generated/PostDropdownQueryFragment.graphql'; import LinkToFullPageNftDetailModal from '~/scenes/NftDetailPage/LinkToFullPageNftDetailModal'; -import colors from '~/shared/theme/colors'; +import { contexts } from '~/shared/analytics/constants'; +import { noop } from '~/shared/utils/noop'; import { getBaseUrl } from '~/utils/getBaseUrl'; import DeletePostConfirmation from './DeletePostConfirmation'; @@ -88,23 +88,28 @@ export default function PostDropdown({ postRef, queryRef }: Props) { - - Share - + {token && ( - - View Item Detail - + )} - - Delete - + ); @@ -114,9 +119,12 @@ export default function PostDropdown({ postRef, queryRef }: Props) { - {}}> - Share - + {/* Follow up: GAL-3862 */} {/* @@ -126,10 +134,9 @@ export default function PostDropdown({ postRef, queryRef }: Props) { - - View Item Detail - + )} diff --git a/apps/web/src/components/Feed/Posts/PostHeader.tsx b/apps/web/src/components/Feed/Posts/PostHeader.tsx index 33d2aa19d2..9bf5194fd6 100644 --- a/apps/web/src/components/Feed/Posts/PostHeader.tsx +++ b/apps/web/src/components/Feed/Posts/PostHeader.tsx @@ -1,15 +1,16 @@ -import unescape from 'lodash/unescape'; +import { useMemo } from 'react'; import { graphql, useFragment } from 'react-relay'; import styled from 'styled-components'; -import Markdown from '~/components/core/Markdown/Markdown'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { BaseM, TitleDiatypeM } from '~/components/core/Text/Text'; import UserHoverCard from '~/components/HoverCard/UserHoverCard'; +import ProcessedText from '~/components/ProcessedText/ProcessedText'; import { ProfilePicture } from '~/components/ProfilePicture/ProfilePicture'; import { PostHeaderFragment$key } from '~/generated/PostHeaderFragment.graphql'; import { PostHeaderQueryFragment$key } from '~/generated/PostHeaderQueryFragment.graphql'; -import { replaceUrlsWithMarkdownFormat } from '~/shared/utils/replaceUrlsWithMarkdownFormat'; +import { contexts } from '~/shared/analytics/constants'; +import { removeNullValues } from '~/shared/relay/removeNullValues'; import { getTimeSince } from '~/shared/utils/time'; import handleCustomDisplayName from '~/utils/handleCustomDisplayName'; @@ -34,6 +35,9 @@ export default function PostHeader({ postRef, queryRef }: Props) { ...UserHoverCardFragment } } + mentions { + ...ProcessedTextFragment + } creationTime ...PostDropdownFragment } @@ -51,6 +55,7 @@ export default function PostHeader({ postRef, queryRef }: Props) { ); const displayName = handleCustomDisplayName(post.author?.username ?? ''); + const nonNullMentions = useMemo(() => removeNullValues(post.mentions), [post.mentions]); return ( @@ -70,7 +75,11 @@ export default function PostHeader({ postRef, queryRef }: Props) { {post.caption && ( - + )} diff --git a/apps/web/src/components/Feed/Posts/PostNftPreview.tsx b/apps/web/src/components/Feed/Posts/PostNftPreview.tsx index 12ccf132b9..c06c718686 100644 --- a/apps/web/src/components/Feed/Posts/PostNftPreview.tsx +++ b/apps/web/src/components/Feed/Posts/PostNftPreview.tsx @@ -9,6 +9,7 @@ import ShimmerProvider from '~/contexts/shimmer/ShimmerContext'; import { PostNftPreviewFragment$key } from '~/generated/PostNftPreviewFragment.graphql'; import { useIsMobileOrMobileLargeWindowWidth } from '~/hooks/useWindowSize'; import { StyledVideo } from '~/scenes/NftDetailPage/NftDetailVideo'; +import { contexts } from '~/shared/analytics/constants'; type Props = { tokenRef: PostNftPreviewFragment$key; @@ -33,7 +34,12 @@ export default function PostNftPreview({ tokenRef, onNftLoad }: Props) { return ( - + ); diff --git a/apps/web/src/components/Feed/Socialize/AdmireButton.tsx b/apps/web/src/components/Feed/Socialize/AdmireButton.tsx index 3f17941d5a..75bd73da5c 100644 --- a/apps/web/src/components/Feed/Socialize/AdmireButton.tsx +++ b/apps/web/src/components/Feed/Socialize/AdmireButton.tsx @@ -7,6 +7,7 @@ import { AdmireButtonFragment$key } from '~/generated/AdmireButtonFragment.graph import { AdmireButtonQueryFragment$key } from '~/generated/AdmireButtonQueryFragment.graphql'; import { AuthModal } from '~/hooks/useAuthModal'; import { AdmireIcon } from '~/icons/SocializeIcons'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; type AdmireButtonProps = { @@ -70,6 +71,12 @@ export function AdmireButton({ eventRef, queryRef, onAdmire, onRemoveAdmire }: A }, [onRemoveAdmire, feedItem.dbid, feedItem.id, feedItem.viewerAdmire?.dbid]); const handleAdmire = useCallback(async () => { + track('Button Click', { + id: 'Admire Button', + name: 'Admire', + context: contexts.Posts, + }); + if (query.viewer?.__typename !== 'Viewer') { showModal({ content: , @@ -79,7 +86,6 @@ export function AdmireButton({ eventRef, queryRef, onAdmire, onRemoveAdmire }: A return; } - track('Admire Click'); onAdmire(); }, [query, track, onAdmire, showModal]); diff --git a/apps/web/src/components/Feed/Socialize/CommentBox/CommentBox.tsx b/apps/web/src/components/Feed/Socialize/CommentBox/CommentBox.tsx index 259099d3fb..85e96ee798 100644 --- a/apps/web/src/components/Feed/Socialize/CommentBox/CommentBox.tsx +++ b/apps/web/src/components/Feed/Socialize/CommentBox/CommentBox.tsx @@ -17,6 +17,7 @@ import { useModalActions } from '~/contexts/modal/ModalContext'; import { useToastActions } from '~/contexts/toast/ToastContext'; import { CommentBoxQueryFragment$key } from '~/generated/CommentBoxQueryFragment.graphql'; import { AuthModal } from '~/hooks/useAuthModal'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import colors from '~/shared/theme/colors'; @@ -86,7 +87,11 @@ export function CommentBox({ queryRef, onSubmitComment, isSubmittingComment }: P return; } - track('Save Comment Click'); + track('Save Comment Click', { + id: 'Submit Comment Button', + name: 'Submit Comment', + context: contexts.Posts, + }); try { onSubmitComment(value); diff --git a/apps/web/src/components/Feed/Socialize/CommentBox/CommentBoxIcon.tsx b/apps/web/src/components/Feed/Socialize/CommentBox/CommentBoxIcon.tsx index dfc4348ca3..edf7eb3ef2 100644 --- a/apps/web/src/components/Feed/Socialize/CommentBox/CommentBoxIcon.tsx +++ b/apps/web/src/components/Feed/Socialize/CommentBox/CommentBoxIcon.tsx @@ -6,6 +6,8 @@ import { CommentBoxIconFragment$key } from '~/generated/CommentBoxIconFragment.g import { CommentBoxIconQueryFragment$key } from '~/generated/CommentBoxIconQueryFragment.graphql'; import { useIsMobileOrMobileLargeWindowWidth } from '~/hooks/useWindowSize'; import { CommentIcon } from '~/icons/SocializeIcons'; +import { contexts } from '~/shared/analytics/constants'; +import { useTrack } from '~/shared/contexts/AnalyticsContext'; import { FeedEventsCommentsModal } from '../CommentsModal/FeedEventsCommentsModal'; import PostCommentsModal from '../CommentsModal/PostCommentsModal'; @@ -50,14 +52,22 @@ export function CommentBoxIcon({ queryRef, eventRef }: Props) { return ; }, [event, isMobile, query]); + const track = useTrack(); + const handleClick = useCallback(() => { + track('Button Click', { + id: 'Open Comments Modal Button', + name: 'Open Comments Modal', + context: contexts.Posts, + }); + showModal({ content: ModalContent, isFullPage: isMobile, isPaddingDisabled: true, headerVariant: 'standard', }); - }, [ModalContent, isMobile, showModal]); + }, [ModalContent, isMobile, showModal, track]); return ; } diff --git a/apps/web/src/components/Feed/Socialize/CommentLine.tsx b/apps/web/src/components/Feed/Socialize/CommentLine.tsx index 42a4a08ac7..8faab435be 100644 --- a/apps/web/src/components/Feed/Socialize/CommentLine.tsx +++ b/apps/web/src/components/Feed/Socialize/CommentLine.tsx @@ -1,17 +1,17 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useFragment } from 'react-relay'; import { graphql } from 'relay-runtime'; import styled from 'styled-components'; -import Markdown from '~/components/core/Markdown/Markdown'; import { HStack } from '~/components/core/Spacer/Stack'; import { BODY_FONT_FAMILY } from '~/components/core/Text/Text'; import UserHoverCard from '~/components/HoverCard/UserHoverCard'; +import ProcessedText from '~/components/ProcessedText/ProcessedText'; import { CommentLineFragment$key } from '~/generated/CommentLineFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; +import { removeNullValues } from '~/shared/relay/removeNullValues'; import colors from '~/shared/theme/colors'; -import { replaceUrlsWithMarkdownFormat } from '~/shared/utils/replaceUrlsWithMarkdownFormat'; import { getTimeSince } from '~/shared/utils/time'; -import unescape from '~/shared/utils/unescape'; type CommentLineProps = { commentRef: CommentLineFragment$key; @@ -30,6 +30,9 @@ export function CommentLine({ commentRef }: CommentLineProps) { username ...UserHoverCardFragment } + mentions { + ...ProcessedTextFragment + } } `, commentRef @@ -45,9 +48,10 @@ export function CommentLine({ commentRef }: CommentLineProps) { }, []); const timeAgo = comment.creationTime ? getTimeSince(comment.creationTime) : null; + const nonNullMentions = useMemo(() => removeNullValues(comment.mentions), [comment.mentions]); return ( - + {comment.commenter && ( @@ -56,12 +60,17 @@ export function CommentLine({ commentRef }: CommentLineProps) { )} - + {timeAgo && {timeAgo}} ); } + const TimeAgoText = styled.div` font-family: ${BODY_FONT_FAMILY}; font-size: 10px; @@ -76,11 +85,10 @@ const StyledUsernameWrapper = styled.div` height: fit-content; `; -const CommenterName = styled.a` +const CommenterName = styled.span` font-family: ${BODY_FONT_FAMILY}; vertical-align: bottom; font-size: 14px; - line-height: 1; font-weight: 700; text-decoration: none; @@ -88,6 +96,12 @@ const CommenterName = styled.a` `; const CommentText = styled.div` + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + font-family: ${BODY_FONT_FAMILY}; font-size: 14px; line-height: 18px; diff --git a/apps/web/src/components/Feed/Socialize/CommentsModal/CommentNote.tsx b/apps/web/src/components/Feed/Socialize/CommentsModal/CommentNote.tsx index e4f8113b0c..2e5c3c6859 100644 --- a/apps/web/src/components/Feed/Socialize/CommentsModal/CommentNote.tsx +++ b/apps/web/src/components/Feed/Socialize/CommentsModal/CommentNote.tsx @@ -1,19 +1,20 @@ +import { useMemo } from 'react'; import { useFragment } from 'react-relay'; import { graphql } from 'relay-runtime'; import styled from 'styled-components'; -import Markdown from '~/components/core/Markdown/Markdown'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { BaseM } from '~/components/core/Text/Text'; import { ListItem } from '~/components/Feed/Socialize/CommentsModal/ListItem'; import { TimeAgoText } from '~/components/Feed/Socialize/CommentsModal/TimeAgoText'; import { UsernameLink } from '~/components/Feed/Socialize/CommentsModal/UsernameLink'; +import ProcessedText from '~/components/ProcessedText/ProcessedText'; import { ProfilePicture } from '~/components/ProfilePicture/ProfilePicture'; import { CommentNoteFragment$key } from '~/generated/CommentNoteFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; +import { removeNullValues } from '~/shared/relay/removeNullValues'; import colors from '~/shared/theme/colors'; -import { replaceUrlsWithMarkdownFormat } from '~/shared/utils/replaceUrlsWithMarkdownFormat'; import { getTimeSince } from '~/shared/utils/time'; -import unescape from '~/shared/utils/unescape'; type CommentNoteProps = { commentRef: CommentNoteFragment$key; @@ -32,12 +33,16 @@ export function CommentNote({ commentRef }: CommentNoteProps) { username ...ProfilePictureFragment } + mentions { + ...ProcessedTextFragment + } } `, commentRef ); const timeAgo = comment.creationTime ? getTimeSince(comment.creationTime) : null; + const nonNullMentions = useMemo(() => removeNullValues(comment.mentions), [comment.mentions]); return ( @@ -50,12 +55,19 @@ export function CommentNote({ commentRef }: CommentNoteProps) { - + {timeAgo} - - - + + + @@ -70,6 +82,15 @@ const StyledListItem = styled(ListItem)` padding: 0px 16px 16px; `; +const StyledBaseM = styled(BaseM)` + word-wrap: break-word; + word-break: break-word; + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +`; + const StyledTimeAgoText = styled(TimeAgoText)` font-size: 10px; `; diff --git a/apps/web/src/components/Feed/Socialize/CommentsModal/UsernameLink.tsx b/apps/web/src/components/Feed/Socialize/CommentsModal/UsernameLink.tsx index 160290dd5d..a4e4f9fd96 100644 --- a/apps/web/src/components/Feed/Socialize/CommentsModal/UsernameLink.tsx +++ b/apps/web/src/components/Feed/Socialize/CommentsModal/UsernameLink.tsx @@ -1,26 +1,27 @@ -import Link from 'next/link'; -import { Route, route } from 'nextjs-routes'; -import styled from 'styled-components'; +import { Route } from 'nextjs-routes'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { TitleS } from '~/components/core/Text/Text'; -import colors from '~/shared/theme/colors'; +import { GalleryElementTrackingProps } from '~/shared/contexts/AnalyticsContext'; -type UsernameLinkProps = { username: string | null }; +type UsernameLinkProps = { + username: string | null; + eventContext: GalleryElementTrackingProps['eventContext']; +}; -export function UsernameLink({ username }: UsernameLinkProps) { +export function UsernameLink({ username, eventContext }: UsernameLinkProps) { const link: Route = username ? { pathname: '/[username]', query: { username } } : { pathname: '/' }; + return ( - - - {username ?? ''} - - + + {username ?? ''} + ); } - -const UsernameLinkWrapper = styled.a` - color: ${colors.black['800']}; - text-decoration: none; -`; diff --git a/apps/web/src/components/Follow/FollowButton.tsx b/apps/web/src/components/Follow/FollowButton.tsx index 17bded6874..01474ed9df 100644 --- a/apps/web/src/components/Follow/FollowButton.tsx +++ b/apps/web/src/components/Follow/FollowButton.tsx @@ -7,6 +7,7 @@ import { useToastActions } from '~/contexts/toast/ToastContext'; import { FollowButtonQueryFragment$key } from '~/generated/FollowButtonQueryFragment.graphql'; import { FollowButtonUserFragment$key } from '~/generated/FollowButtonUserFragment.graphql'; import useAuthModal from '~/hooks/useAuthModal'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import useFollowUser from '~/shared/relay/useFollowUser'; import { useLoggedInUserId } from '~/shared/relay/useLoggedInUserId'; @@ -14,7 +15,7 @@ import useUnfollowUser from '~/shared/relay/useUnfollowUser'; import colors from '~/shared/theme/colors'; import breakpoints from '../core/breakpoints'; -import { Chip } from '../core/Chip/Chip'; +import { GalleryChip } from '../core/Chip/Chip'; type Props = { queryRef: FollowButtonQueryFragment$key; @@ -135,10 +136,24 @@ export default function FollowButton({ queryRef, userRef, className, source }: P return ( // return following & hover show unfollow - Following + + Following + - + Unfollow @@ -146,7 +161,14 @@ export default function FollowButton({ queryRef, userRef, className, source }: P ); } else { return ( - + {followsYou ? 'Follow back' : 'Follow'} ); @@ -171,7 +193,7 @@ export default function FollowButton({ queryRef, userRef, className, source }: P ); } -const FollowingChip = styled(Chip)` +const FollowingChip = styled(GalleryChip)` background-color: ${colors.faint}; color: ${colors.black['800']}; `; @@ -204,12 +226,12 @@ const FollowingChipContainer = styled.div` } `; -const FollowChip = styled(Chip)` +const FollowChip = styled(GalleryChip)` background-color: ${colors.black['800']}; color: ${colors.offWhite}; `; -const UnfollowChip = styled(Chip)` +const UnfollowChip = styled(GalleryChip)` background-color: ${colors.offWhite}; color: #c72905; diff --git a/apps/web/src/components/Follow/FollowListUserItem.tsx b/apps/web/src/components/Follow/FollowListUserItem.tsx index 0be2f18d12..db4bf2b44b 100644 --- a/apps/web/src/components/Follow/FollowListUserItem.tsx +++ b/apps/web/src/components/Follow/FollowListUserItem.tsx @@ -10,9 +10,11 @@ import FollowButton from '~/components/Follow/FollowButton'; import { FollowListUserItemFragment$key } from '~/generated/FollowListUserItemFragment.graphql'; import { FollowListUserItemQueryFragment$key } from '~/generated/FollowListUserItemQueryFragment.graphql'; import { useIsMobileOrMobileLargeWindowWidth } from '~/hooks/useWindowSize'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import { BREAK_LINES } from '~/shared/utils/regex'; +import GalleryLink from '../core/GalleryLink/GalleryLink'; import UserHoverCard from '../HoverCard/UserHoverCard'; import { ProfilePicture } from '../ProfilePicture/ProfilePicture'; @@ -80,6 +82,10 @@ export default function FollowListUserItem({ href={`/${user.username}`} onClick={handleClick} fadeUsernames={fadeUsernames} + eventElementId="Follow List User Item" + eventName="Follow List User Item Click" + // TODO: analytics should be more granular + eventContext={contexts.Social} > @@ -89,7 +95,11 @@ export default function FollowListUserItem({ {formattedUserBio && ( - + )} @@ -103,7 +113,7 @@ export default function FollowListUserItem({ ); } -const StyledListItem = styled.a<{ fadeUsernames: boolean }>` +const StyledListItem = styled(GalleryLink)<{ fadeUsernames: boolean }>` text-decoration: none; display: flex; justify-content: space-between; diff --git a/apps/web/src/components/Follow/NavActionFollow.tsx b/apps/web/src/components/Follow/NavActionFollow.tsx index a664554d85..6217e5e5fd 100644 --- a/apps/web/src/components/Follow/NavActionFollow.tsx +++ b/apps/web/src/components/Follow/NavActionFollow.tsx @@ -1,6 +1,5 @@ -import Link from 'next/link'; import { useRouter } from 'next/router'; -import { Route, route } from 'nextjs-routes'; +import { Route } from 'nextjs-routes'; import { graphql, useFragment } from 'react-relay'; import styled from 'styled-components'; @@ -8,9 +7,11 @@ import { HStack } from '~/components/core/Spacer/Stack'; import { BreadcrumbLink } from '~/contexts/globalLayout/GlobalNavbar/ProfileDropdown/Breadcrumbs'; import { NavActionFollowQueryFragment$key } from '~/generated/NavActionFollowQueryFragment.graphql'; import { NavActionFollowUserFragment$key } from '~/generated/NavActionFollowUserFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import handleCustomDisplayName from '~/utils/handleCustomDisplayName'; +import GalleryLink from '../core/GalleryLink/GalleryLink'; import FollowButton from './FollowButton'; type Props = { @@ -50,14 +51,16 @@ export default function NavActionFollow({ userRef, queryRef }: Props) { return ( - - + + {displayName} - + ); diff --git a/apps/web/src/components/GalleryEditor/CollectionCreateOrEditForm.tsx b/apps/web/src/components/GalleryEditor/CollectionCreateOrEditForm.tsx index 578285d6a4..4842200b9c 100644 --- a/apps/web/src/components/GalleryEditor/CollectionCreateOrEditForm.tsx +++ b/apps/web/src/components/GalleryEditor/CollectionCreateOrEditForm.tsx @@ -8,6 +8,7 @@ import { HStack, VStack } from '~/components/core/Spacer/Stack'; import ErrorText from '~/components/core/Text/ErrorText'; import { TextAreaWithCharCount } from '~/components/core/TextArea/TextArea'; import { useModalActions } from '~/contexts/modal/ModalContext'; +import { contexts } from '~/shared/analytics/constants'; import unescape from '~/shared/utils/unescape'; type Props = { @@ -98,11 +99,24 @@ export function CollectionCreateOrEditForm({ {mode === 'creating' && ( - )} - + diff --git a/apps/web/src/components/GalleryEditor/CollectionEditor/DragAndDrop/DroppableSection.tsx b/apps/web/src/components/GalleryEditor/CollectionEditor/DragAndDrop/DroppableSection.tsx index d465fb8623..02f0561de1 100644 --- a/apps/web/src/components/GalleryEditor/CollectionEditor/DragAndDrop/DroppableSection.tsx +++ b/apps/web/src/components/GalleryEditor/CollectionEditor/DragAndDrop/DroppableSection.tsx @@ -73,54 +73,52 @@ export default function DroppableSection({ children, columns, id, items, style, useKeyDown('ArrowDown', handleArrowDown); return ( - <> - - -
- {children} -
- {isActive && !isDragging && ( - - - - - {step === 5 && ( - - )} - - - )} -
+ + +
+ {children} +
+ {isActive && !isDragging && ( + + + + + {step === 5 && ( + + )} + + + )}
- +
); } diff --git a/apps/web/src/components/GalleryEditor/CollectionEditor/StagingArea.tsx b/apps/web/src/components/GalleryEditor/CollectionEditor/StagingArea.tsx index bf77b05b33..d3d2b632f0 100644 --- a/apps/web/src/components/GalleryEditor/CollectionEditor/StagingArea.tsx +++ b/apps/web/src/components/GalleryEditor/CollectionEditor/StagingArea.tsx @@ -33,6 +33,7 @@ import { useCollectionEditorContext } from '~/contexts/collectionEditor/Collecti import { getImageSizeForColumns } from '~/contexts/collectionEditor/useDndDimensions'; import { StagingAreaFragment$key } from '~/generated/StagingAreaFragment.graphql'; import useKeyDown from '~/hooks/useKeyDown'; +import { contexts } from '~/shared/analytics/constants'; import { removeNullValues } from '~/shared/relay/removeNullValues'; import colors from '~/shared/theme/colors'; import unescape from '~/shared/utils/unescape'; @@ -242,7 +243,7 @@ function StagingArea({ tokensRef }: Props) { - +
diff --git a/apps/web/src/components/GalleryEditor/CollectionSidebar/CollectionListItem.tsx b/apps/web/src/components/GalleryEditor/CollectionSidebar/CollectionListItem.tsx index d794fbc9bd..c89b9d4f5b 100644 --- a/apps/web/src/components/GalleryEditor/CollectionSidebar/CollectionListItem.tsx +++ b/apps/web/src/components/GalleryEditor/CollectionSidebar/CollectionListItem.tsx @@ -8,7 +8,6 @@ import { DropdownSection } from '~/components/core/Dropdown/DropdownSection'; import SettingsDropdown from '~/components/core/Dropdown/SettingsDropdown'; import IconContainer from '~/components/core/IconContainer'; import { HStack } from '~/components/core/Spacer/Stack'; -import { BaseM } from '~/components/core/Text/Text'; import { TitleXS } from '~/components/core/Text/Text'; import { useGalleryEditorContext } from '~/components/GalleryEditor/GalleryEditorContext'; import { NewTooltip } from '~/components/Tooltip/NewTooltip'; @@ -16,6 +15,7 @@ import { useTooltipHover } from '~/components/Tooltip/useTooltipHover'; import { CollectionListItemQueryFragment$key } from '~/generated/CollectionListItemQueryFragment.graphql'; import HideIcon from '~/icons/HideIcon'; import ShowIcon from '~/icons/ShowIcon'; +import { contexts } from '~/shared/analytics/constants'; import { ErrorWithSentryMetadata } from '~/shared/errors/ErrorWithSentryMetadata'; import colors from '~/shared/theme/colors'; import unescape from '~/shared/utils/unescape'; @@ -142,15 +142,25 @@ export function CollectionListItem({ collectionId, queryRef }: CollectionListIte /> - - Edit Name & Description - - - Move To... - - - Delete - + + + diff --git a/apps/web/src/components/GalleryEditor/CollectionSidebar/CollectionSidebar.tsx b/apps/web/src/components/GalleryEditor/CollectionSidebar/CollectionSidebar.tsx index 655040255b..e57a6962f2 100644 --- a/apps/web/src/components/GalleryEditor/CollectionSidebar/CollectionSidebar.tsx +++ b/apps/web/src/components/GalleryEditor/CollectionSidebar/CollectionSidebar.tsx @@ -2,8 +2,8 @@ import { useEffect, useState } from 'react'; import { graphql, useFragment } from 'react-relay'; import styled from 'styled-components'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import IconContainer from '~/components/core/IconContainer'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { TitleS } from '~/components/core/Text/Text'; import { CollectionSearch } from '~/components/GalleryEditor/CollectionSidebar/CollectionSearch'; @@ -15,6 +15,7 @@ import { CollectionSidebarQueryFragment$key } from '~/generated/CollectionSideba import { PaintbrushIcon } from '~/icons/PaintbrushIcon'; import { QuestionMarkIcon } from '~/icons/QuestionMarkIcon'; import { UndoIcon } from '~/icons/UndoIcon'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import OnboardingDialog from '../GalleryOnboardingGuide/OnboardingDialog'; @@ -58,11 +59,14 @@ export function CollectionSidebar({ queryRef }: Props) { - - - - - + + + diff --git a/apps/web/src/components/GalleryEditor/CollectionSidebar/MoveCollectionModal.tsx b/apps/web/src/components/GalleryEditor/CollectionSidebar/MoveCollectionModal.tsx index c5bd70dbad..d7236e9fdd 100644 --- a/apps/web/src/components/GalleryEditor/CollectionSidebar/MoveCollectionModal.tsx +++ b/apps/web/src/components/GalleryEditor/CollectionSidebar/MoveCollectionModal.tsx @@ -10,6 +10,7 @@ import { BaseM, BaseS, TitleDiatypeL } from '~/components/core/Text/Text'; import { useModalActions } from '~/contexts/modal/ModalContext'; import { useToastActions } from '~/contexts/toast/ToastContext'; import { MoveCollectionModalFragment$key } from '~/generated/MoveCollectionModalFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; import { useReportError } from '~/shared/contexts/ErrorReportingContext'; import { removeNullValues } from '~/shared/relay/removeNullValues'; import colors from '~/shared/theme/colors'; @@ -193,6 +194,9 @@ export default function MoveCollectionModal({ )} - + diff --git a/apps/web/src/components/GalleryEditor/GalleryOnboardingGuide/OnboardingDialog.tsx b/apps/web/src/components/GalleryEditor/GalleryOnboardingGuide/OnboardingDialog.tsx index 7775ac71ab..5b623b9e4d 100644 --- a/apps/web/src/components/GalleryEditor/GalleryOnboardingGuide/OnboardingDialog.tsx +++ b/apps/web/src/components/GalleryEditor/GalleryOnboardingGuide/OnboardingDialog.tsx @@ -132,7 +132,13 @@ export default function OnboardingDialog({ step, text, onNext, onClose, options Tip {step} of {FINAL_STEP} - + {step === FINAL_STEP ? 'Finish' : 'Next'}{' '} diff --git a/apps/web/src/components/GalleryEditor/GalleryOnboardingGuide/OnboardingDialogContext.tsx b/apps/web/src/components/GalleryEditor/GalleryOnboardingGuide/OnboardingDialogContext.tsx index 7874f4ccfe..7578903cf0 100644 --- a/apps/web/src/components/GalleryEditor/GalleryOnboardingGuide/OnboardingDialogContext.tsx +++ b/apps/web/src/components/GalleryEditor/GalleryOnboardingGuide/OnboardingDialogContext.tsx @@ -2,6 +2,7 @@ import { createContext, useCallback, useContext, useMemo, useState } from 'react import { graphql, useFragment } from 'react-relay'; import { OnboardingDialogContextFragment$key } from '~/generated/OnboardingDialogContextFragment.graphql'; +import { useTrack } from '~/shared/contexts/AnalyticsContext'; import useExperience from '~/utils/graphql/experiences/useExperience'; import isMac from '~/utils/isMac'; @@ -58,6 +59,8 @@ export function OnboardingDialogProvider({ children, queryRef }: OnboardingDialo queryRef: query, }); + const track = useTrack(); + const dismissUserExperience = useCallback(async () => { // Trick to dismiss the tooltip immediately while waiting for the mutation to finish setStep(0); @@ -65,13 +68,15 @@ export function OnboardingDialogProvider({ children, queryRef }: OnboardingDialo }, [setUserTooltipsExperienced]); const nextStep = useCallback(() => { + track('Web Editor Onboarding Next Step Click', { step }); + if (step === FINAL_STEP) { dismissUserExperience(); return; } setStep(step + 1); - }, [dismissUserExperience, step]); + }, [dismissUserExperience, step, track]); const currentStep = useMemo(() => { if (isUserTooltipsExperienced) return 0; diff --git a/apps/web/src/components/GalleryEditor/PiecesSidebar/AddWalletSidebar.tsx b/apps/web/src/components/GalleryEditor/PiecesSidebar/AddWalletSidebar.tsx index eff5a8e8d8..d4f0af73d8 100644 --- a/apps/web/src/components/GalleryEditor/PiecesSidebar/AddWalletSidebar.tsx +++ b/apps/web/src/components/GalleryEditor/PiecesSidebar/AddWalletSidebar.tsx @@ -8,6 +8,7 @@ import { EmptyState } from '~/components/EmptyState/EmptyState'; import { useModalActions } from '~/contexts/modal/ModalContext'; import { AddWalletSidebarQueryFragment$key } from '~/generated/AddWalletSidebarQueryFragment.graphql'; import ManageWalletsModal from '~/scenes/Modals/ManageWalletsModal'; +import { contexts } from '~/shared/analytics/constants'; import { Chain } from '~/shared/utils/chains'; type Props = { @@ -60,7 +61,13 @@ export function AddWalletSidebar({ handleRefresh, selectedChain, queryRef }: Pro description={`You do not have any ${selectedChain} pieces`} > - diff --git a/apps/web/src/components/GalleryEditor/PiecesSidebar/CreatorEmptyStateSidebar.tsx b/apps/web/src/components/GalleryEditor/PiecesSidebar/CreatorEmptyStateSidebar.tsx index a126158846..1cadab31d2 100644 --- a/apps/web/src/components/GalleryEditor/PiecesSidebar/CreatorEmptyStateSidebar.tsx +++ b/apps/web/src/components/GalleryEditor/PiecesSidebar/CreatorEmptyStateSidebar.tsx @@ -15,6 +15,7 @@ export default function CreatorEmptyStateSidebar() { window.open('https://forms.gle/yJLK93LLw3618Y8y8', '_blank'); }, [track]); + // TODO: this content needs to be updated / changed return ( Are you a creator? @@ -22,7 +23,9 @@ export default function CreatorEmptyStateSidebar() { If you've created onchain work that you'd like to display in your Gallery, please provide details about your project and our team will handle the rest! - + ); } diff --git a/apps/web/src/components/GalleryEditor/PiecesSidebar/PiecesSidebar.tsx b/apps/web/src/components/GalleryEditor/PiecesSidebar/PiecesSidebar.tsx index dee43a7d0d..22a2ea2758 100644 --- a/apps/web/src/components/GalleryEditor/PiecesSidebar/PiecesSidebar.tsx +++ b/apps/web/src/components/GalleryEditor/PiecesSidebar/PiecesSidebar.tsx @@ -14,6 +14,7 @@ import { PiecesSidebarFragment$key } from '~/generated/PiecesSidebarFragment.gra import { PiecesSidebarViewerFragment$key } from '~/generated/PiecesSidebarViewerFragment.graphql'; import useSyncTokens from '~/hooks/api/tokens/useSyncTokens'; import { RefreshIcon } from '~/icons/RefreshIcon'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import { ChainMetadata, chainsMap } from '~/shared/utils/chains'; import { doesUserOwnWalletFromChainFamily } from '~/utils/doesUserOwnWalletFromChainFamily'; @@ -300,7 +301,14 @@ export function PiecesSidebar({ tokensRef, queryRef }: Props) { {!isSearching && shouldDisplayRefreshButtonGroup && ( - + {isLocked ? ( @@ -312,7 +320,13 @@ export function PiecesSidebar({ tokensRef, queryRef }: Props) { )} - + BLANK SPACE diff --git a/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarChainButton.tsx b/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarChainButton.tsx deleted file mode 100644 index f157946001..0000000000 --- a/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarChainButton.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import styled, { css } from 'styled-components'; - -import { HStack } from '~/components/core/Spacer/Stack'; -import { TitleXSBold } from '~/components/core/Text/Text'; -import colors from '~/shared/theme/colors'; -import { ChainMetadata } from '~/shared/utils/chains'; - -type Props = { - chain: ChainMetadata; - onClick: () => void; - isSelected: boolean; -}; - -export function SidebarChainButton({ isSelected, onClick, chain }: Props) { - return ( - - - - {chain.shortName} - - - ); -} -const ChainLogo = styled.img` - width: 16px; - height: 16px; - - margin-right: 4px; -`; - -const ChainButton = styled.div<{ selected: boolean }>` - display: flex; - gap: 0 8px; - padding: 6px 8px; - - z-index: 1; - - border: 1px solid ${colors.black['800']}; - border-radius: 24px; - - position: relative; - align-items: center; - cursor: pointer; - - ${({ selected }) => - selected - ? css` - opacity: 1; - ` - : css` - opacity: 0.5; - border-color: ${colors.porcelain}; - filter: blur(0.05px); - - :hover { - filter: none; - opacity: 1; - } - `} -`; diff --git a/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarChainDropdown.tsx b/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarChainDropdown.tsx index 3b4f846ee1..54f3087120 100644 --- a/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarChainDropdown.tsx +++ b/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarChainDropdown.tsx @@ -11,6 +11,7 @@ import { HStack } from '~/components/core/Spacer/Stack'; import { BaseM } from '~/components/core/Text/Text'; import { SidebarChainDropdownFragment$key } from '~/generated/SidebarChainDropdownFragment.graphql'; import DoubleArrowsIcon from '~/icons/DoubleArrowsIcon'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import { ChainMetadata, chains } from '~/shared/utils/chains'; import isAdminRole from '~/utils/graphql/isAdminRole'; @@ -90,6 +91,9 @@ export default function SidebarChainDropdown({ handleSelectChain(chain); }} disabled={isChainDisabled} + name="Sidebar Chain" + eventContext={contexts.Editor} + eventSelection={chain.name} > {chain.name} diff --git a/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarList/CollectionTitle.tsx b/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarList/CollectionTitle.tsx index 9c60b61095..94d5b81b51 100644 --- a/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarList/CollectionTitle.tsx +++ b/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarList/CollectionTitle.tsx @@ -13,6 +13,7 @@ import OnboardingDialog from '../../GalleryOnboardingGuide/OnboardingDialog'; import { useOnboardingDialogContext } from '../../GalleryOnboardingGuide/OnboardingDialogContext'; import { ExpandedIcon } from '../ExpandedIcon'; import { TokenFilterType } from '../SidebarViewSelector'; +import RefreshContractIcon from './RefreshContractIcon'; import { CollectionTitleRow } from './SidebarList'; import ToggleSpamIcon, { SetSpamFn } from './ToggleSpamIcon'; @@ -37,12 +38,21 @@ export default function CollectionTitle({ const [isMouseHovering, setIsMouseHovering] = useState(false); - const shouldDisplayToggleSpamIcon = useMemo(() => { - if (selectedView === 'Created') { - return false; + const rightContent = useMemo(() => { + if (selectedView === 'Created' && isMouseHovering) { + return ; } - return isMouseHovering; - }, [selectedView, isMouseHovering]); + if (selectedView !== 'Created' && isMouseHovering) { + return ( + + ); + } + return {row.count}; + }, [isMouseHovering, row, selectedView, setSpamPreferenceForCollection]); return ( @@ -59,15 +69,7 @@ export default function CollectionTitle({ {row.title} - {shouldDisplayToggleSpamIcon ? ( - - ) : ( - {row.count} - )} + {rightContent} {step === 4 && index === 0 && ( diff --git a/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarList/RefreshContractIcon.tsx b/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarList/RefreshContractIcon.tsx new file mode 100644 index 0000000000..69ea1bb500 --- /dev/null +++ b/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarList/RefreshContractIcon.tsx @@ -0,0 +1,67 @@ +import { useCallback, useState } from 'react'; +import { useSyncCreatedTokensForExistingContract } from 'src/hooks/api/tokens/useSyncCreatedTokensForExistingContract'; +import styled from 'styled-components'; + +import IconContainer from '~/components/core/IconContainer'; +import Tooltip from '~/components/Tooltip/Tooltip'; +import { RefreshIcon } from '~/icons/RefreshIcon'; +import { contexts } from '~/shared/analytics/constants'; +import { useTrack } from '~/shared/contexts/AnalyticsContext'; + +type Props = { + contractId: string; +}; + +export default function RefreshContractIcon({ contractId }: Props) { + const [showTooltip, setShowTooltip] = useState(false); + + const [syncCreatedTokensForExistingContract, isContractRefreshing] = + useSyncCreatedTokensForExistingContract(); + + const track = useTrack(); + const handleCreatorRefreshContract = useCallback( + async (contractId: string) => { + track('Editor Sidebar: Clicked Sync Creator Tokens For Existing Contract', { + id: 'Refresh Single Created Contract Button', + context: contexts.Editor, + }); + await syncCreatedTokensForExistingContract(contractId); + }, + [track, syncCreatedTokensForExistingContract] + ); + + return ( + <> + setShowTooltip(true)} + onMouseLeave={() => setShowTooltip(false)} + onClick={(e) => { + e.stopPropagation(); + handleCreatorRefreshContract(contractId); + }} + > + } + /> + + + + ); +} + +const ShowRefreshContainer = styled.div` + display: flex; + align-items: center; + margin-left: auto; +`; + +const StyledTooltip = styled(Tooltip)<{ visible: boolean }>` + opacity: ${({ visible }) => (visible ? 1 : 0)}; + width: 110px; + right: 0; + top: 30px; + z-index: 1; +`; diff --git a/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarList/SidebarList.tsx b/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarList/SidebarList.tsx index fd89d7dfc8..3f27be8f69 100644 --- a/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarList/SidebarList.tsx +++ b/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarList/SidebarList.tsx @@ -22,6 +22,7 @@ export type CollectionTitleRow = { type: 'collection-title'; expanded: boolean; address: string; + contractId: string; title: string; count: number; }; diff --git a/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarTokens.tsx b/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarTokens.tsx index 5ee0ba814c..570f3b0e81 100644 --- a/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarTokens.tsx +++ b/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarTokens.tsx @@ -49,7 +49,7 @@ export const SidebarTokens = ({ # Escape hatch for data processing in util files # eslint-disable-next-line relay/unused-fields name - + dbid contractAddress { address } diff --git a/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarViewSelector.tsx b/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarViewSelector.tsx index af9dc8f473..f8e1a634f3 100644 --- a/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarViewSelector.tsx +++ b/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarViewSelector.tsx @@ -8,6 +8,7 @@ import IconContainer from '~/components/core/IconContainer'; import { HStack } from '~/components/core/Spacer/Stack'; import { BaseM } from '~/components/core/Text/Text'; import DoubleArrowsIcon from '~/icons/DoubleArrowsIcon'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; export type TokenFilterType = 'Collected' | 'Created' | 'Hidden'; @@ -48,15 +49,24 @@ export function SidebarViewSelector({ onClose={() => setIsDropdownOpen(false)} > - onSelectView('Collected')}> - Collected - - onSelectView('Created')}> - Created - - onSelectView('Hidden')}> - Hidden - + onSelectView('Collected')} + name="Token Type" + eventContext={contexts.Editor} + label="Collected" + /> + onSelectView('Created')} + name="Token Type" + eventContext={contexts.Editor} + label="Created" + /> + onSelectView('Hidden')} + name="Token Type" + eventContext={contexts.Editor} + label="Hidden" + /> diff --git a/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarWalletSelector.tsx b/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarWalletSelector.tsx index e8e8beed5a..f15507781d 100644 --- a/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarWalletSelector.tsx +++ b/apps/web/src/components/GalleryEditor/PiecesSidebar/SidebarWalletSelector.tsx @@ -12,6 +12,7 @@ import { OnConnectWalletSuccessFn } from '~/components/WalletSelector/multichain import { SidebarWalletSelectorFragment$key } from '~/generated/SidebarWalletSelectorFragment.graphql'; import useAddWalletModal from '~/hooks/useAddWalletModal'; import DoubleArrowsIcon from '~/icons/DoubleArrowsIcon'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import { removeNullValues } from '~/shared/relay/removeNullValues'; import { ChainMetadata } from '~/shared/utils/chains'; @@ -110,13 +111,20 @@ export default function SidebarWalletSelector({ onClose={() => setIsDropdownOpen(false)} > - handleSelectWallet('All')}> - All - + handleSelectWallet('All')} + name="Select Wallet" + eventContext={contexts.Editor} + label="All" + /> {userWalletsOnSelectedNetwork.map((wallet, index) => ( - handleSelectWallet(wallet)}> - {truncateWalletAddress(wallet)} - + handleSelectWallet(wallet)} + name="Select Wallet" + eventContext={contexts.Editor} + label={truncateWalletAddress(wallet)} + /> ))} {!addWalletDisabled && ( - ADD WALLET - + name="Select Wallet" + eventContext={contexts.Editor} + label="Add Wallet" + /> )} diff --git a/apps/web/src/components/GalleryEditor/PiecesSidebar/createVirtualizedRowsFromGroups.ts b/apps/web/src/components/GalleryEditor/PiecesSidebar/createVirtualizedRowsFromGroups.ts index c64bd9f52f..a8df27ac86 100644 --- a/apps/web/src/components/GalleryEditor/PiecesSidebar/createVirtualizedRowsFromGroups.ts +++ b/apps/web/src/components/GalleryEditor/PiecesSidebar/createVirtualizedRowsFromGroups.ts @@ -14,7 +14,7 @@ export function createVirtualizedRowsFromGroups({ const rows: VirtualizedRow[] = []; for (const group of groups) { - const { title, address, tokens } = group; + const { title, address, contractId, tokens } = group; // Default to expanded const expanded = !collapsedCollections.has(address); @@ -23,6 +23,7 @@ export function createVirtualizedRowsFromGroups({ type: 'collection-title', expanded, address: address, + contractId: contractId, title: title, count: tokens.length, }); diff --git a/apps/web/src/components/GalleryEditor/PiecesSidebar/groupCollectionsByAddress.ts b/apps/web/src/components/GalleryEditor/PiecesSidebar/groupCollectionsByAddress.ts index c6b115cb34..2616c2450e 100644 --- a/apps/web/src/components/GalleryEditor/PiecesSidebar/groupCollectionsByAddress.ts +++ b/apps/web/src/components/GalleryEditor/PiecesSidebar/groupCollectionsByAddress.ts @@ -3,6 +3,7 @@ import { SidebarTokensFragment$data } from '~/generated/SidebarTokensFragment.gr export type CollectionGroup = { title: string; address: string; + contractId: string; // Remove the readonly tokens: Array; }; @@ -36,6 +37,7 @@ export function groupCollectionsByAddress({ title, tokens: [], address: token.contract.contractAddress.address, + contractId: token.contract.dbid, }; map[address] = group; diff --git a/apps/web/src/components/GalleryPill.tsx b/apps/web/src/components/GalleryPill.tsx new file mode 100644 index 0000000000..75bb8229f9 --- /dev/null +++ b/apps/web/src/components/GalleryPill.tsx @@ -0,0 +1,84 @@ +import { ButtonHTMLAttributes, MouseEventHandler, useCallback } from 'react'; +import styled from 'styled-components'; + +import GalleryLink, { GalleryLinkProps } from '~/components/core/GalleryLink/GalleryLink'; +import { GalleryElementTrackingProps, useTrack } from '~/shared/contexts/AnalyticsContext'; +import colors from '~/shared/theme/colors'; + +type GalleryPillProps = { + active?: boolean; + className?: string; + disabled?: boolean; +} & GalleryLinkProps & + ButtonHTMLAttributes & + GalleryElementTrackingProps; + +/** + * This component will either render an GalleryLink for redirects, + * or a simple Button + */ +export function GalleryPill(props: GalleryPillProps) { + const track = useTrack(); + + const { to, href, eventElementId, eventName, eventContext, eventFlow, properties, onClick } = + props; + + const handleClick = useCallback>( + (event) => { + event.stopPropagation(); + + track('Pill Click', { + id: eventElementId, + name: eventName, + context: eventContext, + flow: eventFlow, + ...properties, + }); + + onClick?.(event); + }, + [eventContext, eventElementId, eventFlow, eventName, onClick, properties, track] + ); + + if (to || href) { + // @ts-expect-error fix this later. the problem is we're overloading both anchor props and button props into GalleryPillProps. might need to split up this component into GalleryPillLink and GalleryPillButton + return ; + } + + // @ts-expect-error fix this later. the problem is we're overloading both anchor props and button props into GalleryPillProps. might need to split up this component into GalleryPillLink and GalleryPillButton + return ; +} + +type StyledComponentProps = { + active?: boolean; + disabled?: boolean; +}; + +const sharedStyles = ({ active, disabled }: StyledComponentProps) => ` + border: 1px solid ${colors.porcelain}; + background-color: ${colors.white}; + padding: 0 12px; + border-radius: 24px; + color: ${colors.black['800']}; + text-decoration: none; + width: fit-content; + max-width: 100%; + height: 32px; + display: flex; + align-items: center; + cursor: pointer; + + &:hover { + border-color: ${disabled ? colors.porcelain : colors.black['800']}; + } + + ${active ? `border-color: ${colors.black['800']};` : ''} +`; + +const GalleryPillLink = styled(GalleryLink)` + ${(props) => sharedStyles(props)} +`; + +const GalleryPillButton = styled.button` + ${(props) => sharedStyles(props)} +`; diff --git a/apps/web/src/components/HoverCard/CommunityHoverCard.tsx b/apps/web/src/components/HoverCard/CommunityHoverCard.tsx index e9472cd84b..064e3b4728 100644 --- a/apps/web/src/components/HoverCard/CommunityHoverCard.tsx +++ b/apps/web/src/components/HoverCard/CommunityHoverCard.tsx @@ -1,4 +1,3 @@ -import Link from 'next/link'; import { Route } from 'nextjs-routes'; import { PropsWithChildren, useCallback, useMemo } from 'react'; import { PreloadedQuery, useFragment, usePreloadedQuery, useQueryLoader } from 'react-relay'; @@ -7,12 +6,13 @@ import styled from 'styled-components'; import { CommunityHoverCardFragment$key } from '~/generated/CommunityHoverCardFragment.graphql'; import { Chain, CommunityHoverCardQuery } from '~/generated/CommunityHoverCardQuery.graphql'; +import { contexts } from '~/shared/analytics/constants'; import { ErrorWithSentryMetadata } from '~/shared/errors/ErrorWithSentryMetadata'; import { removeNullValues } from '~/shared/relay/removeNullValues'; import { useLoggedInUserId } from '~/shared/relay/useLoggedInUserId'; import colors from '~/shared/theme/colors'; -import InteractiveLink from '../core/InteractiveLink/InteractiveLink'; +import GalleryLink from '../core/GalleryLink/GalleryLink'; import Markdown from '../core/Markdown/Markdown'; import { HStack, VStack } from '../core/Spacer/Stack'; import { BaseM, BaseS, TitleM } from '../core/Text/Text'; @@ -156,7 +156,7 @@ function CommunityHoverCardContent({ const content = useMemo(() => { const result = ownersToDisplay.map((owner) => ( - {loggedInUserId === owner.id ? 'You' : owner.username} - + )); if (totalOwners > 3) { @@ -202,17 +202,25 @@ function CommunityHoverCardContent({ return ( - + - +
- + {communityName} - + {community.description && ( - + )} @@ -229,12 +237,6 @@ function CommunityHoverCardContent({ ); } -const StyledLink = styled(Link)` - text-decoration: none; - outline: none; - min-width: 0; -`; - const Section = styled(VStack)` max-width: 250px; `; @@ -258,7 +260,7 @@ const StyledCardDescription = styled.div` } `; -const StyledInteractiveLink = styled(InteractiveLink)` +const StyledGalleryLink = styled(GalleryLink)` font-size: 12px; `; diff --git a/apps/web/src/components/HoverCard/HoverCard.tsx b/apps/web/src/components/HoverCard/HoverCard.tsx index 54bbfb74c9..9d98679d01 100644 --- a/apps/web/src/components/HoverCard/HoverCard.tsx +++ b/apps/web/src/components/HoverCard/HoverCard.tsx @@ -11,17 +11,18 @@ import { useRole, } from '@floating-ui/react'; import { AnimatePresence, motion } from 'framer-motion'; -import Link from 'next/link'; import { Route } from 'nextjs-routes'; import { MouseEventHandler, Suspense, useCallback, useEffect, useId, useState } from 'react'; import { PreloadedQuery } from 'react-relay'; import { OperationType } from 'relay-runtime'; import styled from 'styled-components'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import { noop } from '~/shared/utils/noop'; import breakpoints, { pageGutter } from '../core/breakpoints'; +import GalleryLink from '../core/GalleryLink/GalleryLink'; import { SelfCenteredSpinner } from '../core/Spinner/Spinner'; import { ANIMATED_COMPONENT_TRANSITION_S, @@ -81,9 +82,15 @@ export default function HoverCard({ return ( - + {HoverableElement} - + @@ -126,18 +133,12 @@ const StyledContainer = styled.span` position: relative; display: inline-grid; cursor: initial; - width: 100%; `; -const StyledLinkContainer = styled.div` +const StyledLinkContainer = styled.span` display: inline-flex; `; -const StyledLink = styled(Link)` - text-decoration: none; - width: 100%; -`; - const StyledCardContainer = styled.div` border: 1px solid ${colors.black['800']}; padding: 16px; diff --git a/apps/web/src/components/HoverCard/UserHoverCard.tsx b/apps/web/src/components/HoverCard/UserHoverCard.tsx index b53ed82f59..d1ad6c6461 100644 --- a/apps/web/src/components/HoverCard/UserHoverCard.tsx +++ b/apps/web/src/components/HoverCard/UserHoverCard.tsx @@ -1,5 +1,4 @@ import unescape from 'lodash/unescape'; -import Link from 'next/link'; import { Route } from 'nextjs-routes'; import { PropsWithChildren, useCallback, useMemo } from 'react'; import { @@ -16,11 +15,13 @@ import { UserHoverCardFragment$key } from '~/generated/UserHoverCardFragment.gra import { COMMUNITIES_PER_PAGE } from '~/scenes/UserGalleryPage/UserSharedInfo/UserSharedCommunities'; import UserSharedInfo from '~/scenes/UserGalleryPage/UserSharedInfo/UserSharedInfo'; import { FOLLOWERS_PER_PAGE } from '~/scenes/UserGalleryPage/UserSharedInfo/UserSharedInfoList/SharedFollowersList'; +import { contexts } from '~/shared/analytics/constants'; import { ErrorWithSentryMetadata } from '~/shared/errors/ErrorWithSentryMetadata'; import { useLoggedInUserId } from '~/shared/relay/useLoggedInUserId'; import handleCustomDisplayName from '~/utils/handleCustomDisplayName'; import Badge from '../Badge/Badge'; +import GalleryLink from '../core/GalleryLink/GalleryLink'; import Markdown from '../core/Markdown/Markdown'; import { HStack, VStack } from '../core/Spacer/Stack'; import { BaseM, TitleDiatypeM, TitleM } from '../core/Text/Text'; @@ -157,17 +158,22 @@ function UserHoverCardContent({ - + {displayName} - + {userBadges.map((badge) => ( // Might need to rethink this layout when we have more badges - + ))} @@ -182,7 +188,7 @@ function UserHoverCardContent({ {user.bio && ( - + )} @@ -208,12 +214,6 @@ const StyledCardHeaderContainer = styled(VStack)` padding-bottom: 12px; `; -const StyledLink = styled(Link)` - text-decoration: none; - outline: none; - min-width: 0; -`; - const StyledUsernameAndBadge = styled(HStack)` min-width: 0; `; diff --git a/apps/web/src/components/LinkableAddress.tsx b/apps/web/src/components/LinkableAddress.tsx index 2f8ff80ec6..ccb8fc9b51 100644 --- a/apps/web/src/components/LinkableAddress.tsx +++ b/apps/web/src/components/LinkableAddress.tsx @@ -1,16 +1,18 @@ import { useFragment } from 'react-relay'; import { graphql } from 'relay-runtime'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { BaseM } from '~/components/core/Text/Text'; import { LinkableAddressFragment$key } from '~/generated/LinkableAddressFragment.graphql'; +import { GalleryElementTrackingProps } from '~/shared/contexts/AnalyticsContext'; import { getExternalAddressLink, graphqlTruncateAddress } from '~/shared/utils/wallet'; type LinkableAddressProps = { chainAddressRef: LinkableAddressFragment$key; + eventContext: GalleryElementTrackingProps['eventContext']; }; -export function LinkableAddress({ chainAddressRef }: LinkableAddressProps) { +export function LinkableAddress({ chainAddressRef, eventContext }: LinkableAddressProps) { const address = useFragment( graphql` fragment LinkableAddressFragment on ChainAddress { @@ -31,7 +33,12 @@ export function LinkableAddress({ chainAddressRef }: LinkableAddressProps) { } return ( - + ); } @@ -39,8 +46,23 @@ type RawLinkableAddressProps = { link: string; address: string; truncatedAddress: string | null; + eventContext: GalleryElementTrackingProps['eventContext']; }; -export function RawLinkableAddress({ link, truncatedAddress, address }: RawLinkableAddressProps) { - return {truncatedAddress || address}; +export function RawLinkableAddress({ + link, + truncatedAddress, + address, + eventContext, +}: RawLinkableAddressProps) { + return ( + + {truncatedAddress || address} + + ); } diff --git a/apps/web/src/components/ManageWallets/ManageWallets.tsx b/apps/web/src/components/ManageWallets/ManageWallets.tsx index f0e87925ee..30e7867632 100644 --- a/apps/web/src/components/ManageWallets/ManageWallets.tsx +++ b/apps/web/src/components/ManageWallets/ManageWallets.tsx @@ -9,6 +9,7 @@ import SettingsRowDescription from '~/components/Settings/SettingsRowDescription import { useToastActions } from '~/contexts/toast/ToastContext'; import { ManageWalletsFragment$key } from '~/generated/ManageWalletsFragment.graphql'; import useAddWalletModal from '~/hooks/useAddWalletModal'; +import { contexts } from '~/shared/analytics/constants'; import { removeNullValues } from '~/shared/relay/removeNullValues'; import { graphqlTruncateAddress, truncateAddress } from '~/shared/utils/wallet'; @@ -132,7 +133,14 @@ function ManageWallets({ newAddress, queryRef, onConnectWalletSuccess }: Props) - + Add new wallet diff --git a/apps/web/src/components/ManageWallets/ManageWalletsRow.tsx b/apps/web/src/components/ManageWallets/ManageWalletsRow.tsx index 0d5730f0de..656097abd7 100644 --- a/apps/web/src/components/ManageWallets/ManageWalletsRow.tsx +++ b/apps/web/src/components/ManageWallets/ManageWalletsRow.tsx @@ -7,6 +7,7 @@ import { BaseM, BODY_MONO_FONT_FAMILY } from '~/components/core/Text/Text'; import { walletIconMap } from '~/components/WalletSelector/multichain/WalletButton'; import useRemoveWallet from '~/components/WalletSelector/mutations/useRemoveWallet'; import { ManageWalletsRow$key } from '~/generated/ManageWalletsRow.graphql'; +import { contexts } from '~/shared/analytics/constants'; import { graphqlTruncateAddress } from '~/shared/utils/wallet'; import { isWeb3Error } from '~/types/Error'; @@ -81,6 +82,9 @@ function ManageWalletsRow({ Are you sure you want to delete this gallery? - + Delete diff --git a/apps/web/src/components/MultiGallery/Gallery.tsx b/apps/web/src/components/MultiGallery/Gallery.tsx index 7e7b9b011e..bef1818c5f 100644 --- a/apps/web/src/components/MultiGallery/Gallery.tsx +++ b/apps/web/src/components/MultiGallery/Gallery.tsx @@ -14,6 +14,7 @@ import { useIsMobileWindowWidth } from '~/hooks/useWindowSize'; import ArrowDownIcon from '~/icons/ArrowDownIcon'; import ArrowUpIcon from '~/icons/ArrowUpIcon'; import DragHandleIcon from '~/icons/DragHandleIcon'; +import { contexts } from '~/shared/analytics/constants'; import { removeNullValues } from '~/shared/relay/removeNullValues'; import { useLoggedInUserId } from '~/shared/relay/useLoggedInUserId'; import colors from '~/shared/theme/colors'; @@ -308,26 +309,44 @@ export default function Gallery({ - - Edit Name & Description - + {hidden ? ( - UNHIDE + ) : ( <> {!isFeatured && ( - - Feature on Profile - + )} - - Hide - + )} - - Delete - + diff --git a/apps/web/src/components/NftPreview/CollectionTokenPreview.tsx b/apps/web/src/components/NftPreview/CollectionTokenPreview.tsx index e0e7c3fc64..63888d5074 100644 --- a/apps/web/src/components/NftPreview/CollectionTokenPreview.tsx +++ b/apps/web/src/components/NftPreview/CollectionTokenPreview.tsx @@ -2,6 +2,7 @@ import { graphql, useFragment } from 'react-relay'; import { CollectionTokenPreviewFragment$key } from '~/generated/CollectionTokenPreviewFragment.graphql'; import { useIsMobileOrMobileLargeWindowWidth } from '~/hooks/useWindowSize'; +import { contexts } from '~/shared/analytics/constants'; import NftPreview from './NftPreview'; @@ -50,6 +51,7 @@ export default function CollectionTokenPreview({ tokenRef={collectionToken.token} shouldLiveRender={shouldLiveRender} collectionId={collection.dbid} + eventContext={contexts.UserCollection} /> ); } diff --git a/apps/web/src/components/NftPreview/NftPreview.tsx b/apps/web/src/components/NftPreview/NftPreview.tsx index b454a4eba4..d044a66218 100644 --- a/apps/web/src/components/NftPreview/NftPreview.tsx +++ b/apps/web/src/components/NftPreview/NftPreview.tsx @@ -14,7 +14,9 @@ import NftDetailAnimation from '~/scenes/NftDetailPage/NftDetailAnimation'; import NftDetailGif from '~/scenes/NftDetailPage/NftDetailGif'; import NftDetailModel from '~/scenes/NftDetailPage/NftDetailModel'; import NftDetailVideo from '~/scenes/NftDetailPage/NftDetailVideo'; +import { GalleryElementTrackingProps } from '~/shared/contexts/AnalyticsContext'; import { useGetSinglePreviewImage } from '~/shared/relay/useGetPreviewImages'; +import { isKnownComputeIntensiveToken } from '~/shared/utils/prohibition'; import { isFirefox, isSafari } from '~/utils/browser'; import isSvg from '~/utils/isSvg'; import { getBackgroundColorOverrideForContract } from '~/utils/token'; @@ -32,6 +34,7 @@ type Props = { shouldLiveRender?: boolean; collectionId?: string; onLoad?: () => void; + eventContext: GalleryElementTrackingProps['eventContext']; }; const contractsWhoseIFrameNFTsShouldNotTakeUpFullHeight = new Set([ @@ -43,14 +46,16 @@ function NftPreview({ disableLiverender = false, columns = 3, isInFeedEvent = false, - shouldLiveRender, + shouldLiveRender: _shouldLiveRender, collectionId, onLoad, + eventContext, }: Props) { const token = useFragment( graphql` fragment NftPreviewFragment on Token { dbid + tokenId contract { contractAddress { address @@ -88,6 +93,7 @@ function NftPreview({ const ownerUsername = token.owner?.username; + const tokenId = token.tokenId ?? ''; const contractAddress = token.contract?.contractAddress?.address ?? ''; const backgroundColorOverride = useMemo( @@ -95,6 +101,10 @@ function NftPreview({ [contractAddress] ); + const shouldLiveRender = isKnownComputeIntensiveToken(contractAddress, tokenId) + ? false + : _shouldLiveRender; + const isIFrameLiveDisplay = Boolean( (shouldLiveRender && token.media?.__typename === 'HtmlMedia') || (shouldLiveRender && token.media?.__typename === 'GltfMedia') @@ -175,6 +185,7 @@ function NftPreview({ username={ownerUsername ?? ''} collectionId={collectionId} tokenId={token.dbid} + eventContext={eventContext} > + {collectionName} - + ) : ( @@ -109,7 +115,7 @@ function CollectionName({ tokenRef, interactive }: CollectionNameProps) { } return shouldDisplayLinkToCommunityPage ? ( - + {token.contract?.badgeURL && } @@ -126,7 +132,7 @@ function CollectionName({ tokenRef, interactive }: CollectionNameProps) { )} - + ) : ( @@ -167,7 +173,7 @@ const POAPLogo = styled.img.attrs({ height: 20px; `; -const StyledInteractiveLink = styled(InteractiveLink)` +const StyledGalleryLink = styled(GalleryLink)` text-decoration: none; `; diff --git a/apps/web/src/components/NftSelector/NftSelector.tsx b/apps/web/src/components/NftSelector/NftSelector.tsx index dfbc0c695e..77b3ff0b37 100644 --- a/apps/web/src/components/NftSelector/NftSelector.tsx +++ b/apps/web/src/components/NftSelector/NftSelector.tsx @@ -1,5 +1,6 @@ import { Suspense, useCallback, useEffect, useMemo } from 'react'; import { graphql, useFragment, useLazyLoadQuery } from 'react-relay'; +import { useSyncCreatedTokensForExistingContract } from 'src/hooks/api/tokens/useSyncCreatedTokensForExistingContract'; import styled from 'styled-components'; import { usePostComposerContext } from '~/contexts/postComposer/PostComposerContext'; @@ -8,7 +9,8 @@ import { NftSelectorViewerFragment$key } from '~/generated/NftSelectorViewerFrag import useSyncTokens from '~/hooks/api/tokens/useSyncTokens'; import { ChevronLeftIcon } from '~/icons/ChevronLeftIcon'; import { RefreshIcon } from '~/icons/RefreshIcon'; -import { useTrack } from '~/shared/contexts/AnalyticsContext'; +import { contexts } from '~/shared/analytics/constants'; +import { GalleryElementTrackingProps, useTrack } from '~/shared/contexts/AnalyticsContext'; import { removeNullValues } from '~/shared/relay/removeNullValues'; import { doesUserOwnWalletFromChainFamily } from '~/utils/doesUserOwnWalletFromChainFamily'; @@ -32,6 +34,7 @@ type Props = { onSelectToken: (tokenId: string) => void; headerText: string; preSelectedContract?: NftSelectorContractType; + eventFlow?: GalleryElementTrackingProps['eventFlow']; }; export type NftSelectorContractType = Omit | null; @@ -44,7 +47,7 @@ export function NftSelector(props: Props) { ); } -function NftSelectorInner({ onSelectToken, headerText, preSelectedContract }: Props) { +function NftSelectorInner({ onSelectToken, headerText, preSelectedContract, eventFlow }: Props) { const query = useLazyLoadQuery( graphql` query NftSelectorQuery { @@ -82,6 +85,7 @@ function NftSelectorInner({ onSelectToken, headerText, preSelectedContract }: Pr ownerIsCreator contract { + dbid name } @@ -99,7 +103,6 @@ function NftSelectorInner({ onSelectToken, headerText, preSelectedContract }: Pr ); const tokens = useMemo(() => removeNullValues(viewer?.user?.tokens), [viewer?.user?.tokens]); - const { searchQuery, setSearchQuery, tokenSearchResults, isSearching } = useTokenSearchResults< (typeof tokens)[0] >({ @@ -118,6 +121,19 @@ function NftSelectorInner({ onSelectToken, headerText, preSelectedContract }: Pr setSelectedContract, } = usePostComposerContext(); + const track = useTrack(); + + const handleSelectContract = useCallback( + (contract: NftSelectorContractType) => { + track('Select Contract on Post Composer Modal', { + context: contexts.Posts, + flow: eventFlow, + }); + setSelectedContract(contract); + }, + [eventFlow, setSelectedContract, track] + ); + useEffect(() => { if (preSelectedContract) { setSelectedContract(preSelectedContract); @@ -198,7 +214,6 @@ function NftSelectorInner({ onSelectToken, headerText, preSelectedContract }: Pr const ownsWalletFromSelectedChainFamily = doesUserOwnWalletFromChainFamily(network, query); - const track = useTrack(); const isRefreshDisabledAtUserLevel = isRefreshDisabledForUser(viewer?.user?.dbid ?? ''); const refreshDisabled = isRefreshDisabledAtUserLevel || !ownsWalletFromSelectedChainFamily || isLocked; @@ -208,14 +223,33 @@ function NftSelectorInner({ onSelectToken, headerText, preSelectedContract }: Pr return; } - track('NFT Selector: Clicked Refresh'); + track('NFT Selector: Clicked Refresh', { + context: contexts.Posts, + flow: eventFlow, + }); if (filterType === 'Hidden') { return; } await syncTokens({ type: filterType, chain: network }); - }, [refreshDisabled, track, filterType, syncTokens, network]); + }, [refreshDisabled, track, eventFlow, filterType, syncTokens, network]); + + const [syncCreatedTokensForExistingContract, isContractRefreshing] = + useSyncCreatedTokensForExistingContract(); + const contractRefreshDisabled = filterType !== 'Created' || isContractRefreshing; + + const handleCreatorRefreshContract = useCallback(async () => { + if (!selectedContract?.dbid) { + return; + } + track('NFT Selector: Clicked Sync Creator Tokens For Existing Contract', { + id: 'Refresh Single Created Tokens Contract Button', + context: contexts.Posts, + }); + + await syncCreatedTokensForExistingContract(selectedContract?.dbid); + }, [selectedContract?.dbid, track, syncCreatedTokensForExistingContract]); const { floating, reference, getFloatingProps, getReferenceProps, floatingStyle } = useTooltipHover({ @@ -257,8 +291,26 @@ function NftSelectorInner({ onSelectToken, headerText, preSelectedContract }: Pr {selectedContract ? ( {selectedContract?.title} - - + + + } + ref={reference} + {...getReferenceProps()} + /> + + + ) : ( @@ -304,11 +356,12 @@ function NftSelectorInner({ onSelectToken, headerText, preSelectedContract }: Pr )} diff --git a/apps/web/src/components/NftSelector/NftSelectorFilter/NftSelectorFilterNetwork.tsx b/apps/web/src/components/NftSelector/NftSelectorFilter/NftSelectorFilterNetwork.tsx index 35e76cc57b..608ca3864c 100644 --- a/apps/web/src/components/NftSelector/NftSelectorFilter/NftSelectorFilterNetwork.tsx +++ b/apps/web/src/components/NftSelector/NftSelectorFilter/NftSelectorFilterNetwork.tsx @@ -7,6 +7,7 @@ import { BaseM } from '~/components/core/Text/Text'; import { TokenFilterType } from '~/components/GalleryEditor/PiecesSidebar/SidebarViewSelector'; import { NftSelectorFilterNetworkFragment$key } from '~/generated/NftSelectorFilterNetworkFragment.graphql'; import DoubleArrowsIcon from '~/icons/DoubleArrowsIcon'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import colors from '~/shared/theme/colors'; import { Chain, ChainMetadata, chains } from '~/shared/utils/chains'; @@ -107,6 +108,9 @@ export function NftSelectorFilterNetwork({ onSelectChain(chain); }} disabled={isChainDisabled} + name="NFT Selector Filter Network" + eventContext={contexts.Posts} + eventSelection={chain.name} > diff --git a/apps/web/src/components/NftSelector/NftSelectorFilter/NftSelectorFilterSort.tsx b/apps/web/src/components/NftSelector/NftSelectorFilter/NftSelectorFilterSort.tsx index d20d632550..6d34262ff8 100644 --- a/apps/web/src/components/NftSelector/NftSelectorFilter/NftSelectorFilterSort.tsx +++ b/apps/web/src/components/NftSelector/NftSelectorFilter/NftSelectorFilterSort.tsx @@ -2,6 +2,7 @@ import { useCallback, useState } from 'react'; import styled from 'styled-components'; import DoubleArrowsIcon from '~/icons/DoubleArrowsIcon'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import colors from '~/shared/theme/colors'; @@ -44,15 +45,24 @@ export function NftSelectorFilterSort({ setIsDropdownOpen(false)}> - onSelectView('Recently added')}> - Recently added - - onSelectView('Oldest')}> - Oldest - - onSelectView('Alphabetical')}> - Alphabetical - + onSelectView('Recently added')} + name="NFT Selector Filter Sort" + eventContext={contexts.Posts} + label="Recently added" + /> + onSelectView('Oldest')} + name="NFT Selector Filter Sort" + eventContext={contexts.Posts} + label="Oldest" + /> + onSelectView('Alphabetical')} + name="NFT Selector Filter Sort" + eventContext={contexts.Posts} + label="Alphabetical" + /> diff --git a/apps/web/src/components/NftSelector/NftSelectorFilter/NftSelectorViewSelector.tsx b/apps/web/src/components/NftSelector/NftSelectorFilter/NftSelectorViewSelector.tsx index 0ce521e519..e82996f1cf 100644 --- a/apps/web/src/components/NftSelector/NftSelectorFilter/NftSelectorViewSelector.tsx +++ b/apps/web/src/components/NftSelector/NftSelectorFilter/NftSelectorViewSelector.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import DoubleArrowsIcon from '~/icons/DoubleArrowsIcon'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import colors from '~/shared/theme/colors'; import { Chain, chains } from '~/shared/utils/chains'; @@ -58,15 +59,19 @@ export function NftSelectorViewSelector({ setIsDropdownOpen(false)}> - onSelectView('Collected')}> - Collected - + onSelectView('Collected')} + name="NFT Selector Filter Type" + eventContext={contexts.Posts} + label="Collected" + /> onSelectView('Created')} disabled={!isCreatorSupportEnabledForChain} - > - Created - + name="NFT Selector Filter Type" + eventContext={contexts.Posts} + label="Created" + /> diff --git a/apps/web/src/components/NftSelector/NftSelectorPreviewAsset.tsx b/apps/web/src/components/NftSelector/NftSelectorPreviewAsset.tsx index 62e0b3d8b1..b3d700445a 100644 --- a/apps/web/src/components/NftSelector/NftSelectorPreviewAsset.tsx +++ b/apps/web/src/components/NftSelector/NftSelectorPreviewAsset.tsx @@ -9,9 +9,13 @@ import transitions from '../core/transitions'; type NftSelectorPreviewAssetProps = { tokenRef: NftSelectorPreviewAssetFragment$key; + enforceSquareAspectRatio?: boolean; }; -export function NftSelectorPreviewAsset({ tokenRef }: NftSelectorPreviewAssetProps) { +export function NftSelectorPreviewAsset({ + tokenRef, + enforceSquareAspectRatio = true, +}: NftSelectorPreviewAssetProps) { const token = useFragment( graphql` fragment NftSelectorPreviewAssetFragment on Token { @@ -25,12 +29,20 @@ export function NftSelectorPreviewAsset({ tokenRef }: NftSelectorPreviewAssetPro const imageUrl = useGetSinglePreviewImage({ tokenRef: token, size: 'medium' }) ?? ''; const { handleNftLoaded } = useNftRetry({ tokenId: token.dbid }); - - return ; + return ( + + ); } type SelectedProps = { isSelected?: boolean; + enforceSquareAspectRatio: boolean; }; const StyledImage = styled.img` @@ -38,9 +50,14 @@ const StyledImage = styled.img` max-width: 100%; transition: opacity ${transitions.cubic}; opacity: ${({ isSelected }) => (isSelected ? 0.5 : 1)}; - - height: auto; - width: 100%; - aspect-ratio: 1 / 1; object-fit: cover; + + ${({ enforceSquareAspectRatio }) => + enforceSquareAspectRatio + ? ` + height: auto; + width: 100%; + aspect-ratio: 1 / 1; + ` + : undefined} `; diff --git a/apps/web/src/components/NftSelector/NftSelectorTokenPreview.tsx b/apps/web/src/components/NftSelector/NftSelectorTokenPreview.tsx index ca34326bca..28113ec353 100644 --- a/apps/web/src/components/NftSelector/NftSelectorTokenPreview.tsx +++ b/apps/web/src/components/NftSelector/NftSelectorTokenPreview.tsx @@ -2,7 +2,6 @@ import { useCallback, useMemo } from 'react'; import styled, { css } from 'styled-components'; import colors from '~/shared/theme/colors'; -import { noop } from '~/shared/utils/noop'; import { VStack } from '../core/Spacer/Stack'; import { BaseM } from '../core/Text/Text'; @@ -14,7 +13,7 @@ import { NftSelectorToken } from './NftSelectorToken'; type Props = { group: NftSelectorCollectionGroup; onSelectToken: (tokenId: string) => void; - onSelectContract?: (collection: NftSelectorContractType) => void; + onSelectContract: (collection: NftSelectorContractType) => void; hasSelectedContract: boolean; }; @@ -29,19 +28,20 @@ const NftSelectorTokenCollection = ({ title }: { title: string }) => { export function NftSelectorTokenPreview({ group, onSelectToken, - onSelectContract = noop, + onSelectContract, hasSelectedContract, }: Props) { const tokens = group.tokens; const handleSelectContract = useCallback(() => { const collection = { + dbid: group.dbid, title: group.title, address: group.address, }; onSelectContract(collection); - }, [group.address, group.title, onSelectContract]); + }, [group.address, group.title, group.dbid, onSelectContract]); const singleTokenTitle = useMemo(() => { const token = tokens[0]; diff --git a/apps/web/src/components/NftSelector/NftSelectorView.tsx b/apps/web/src/components/NftSelector/NftSelectorView.tsx index 3424abc8a4..61a1f0a4ce 100644 --- a/apps/web/src/components/NftSelector/NftSelectorView.tsx +++ b/apps/web/src/components/NftSelector/NftSelectorView.tsx @@ -7,6 +7,8 @@ import { useModalActions } from '~/contexts/modal/ModalContext'; import { NftSelectorViewFragment$key } from '~/generated/NftSelectorViewFragment.graphql'; import useAddWalletModal from '~/hooks/useAddWalletModal'; import useWindowSize, { useIsMobileWindowWidth } from '~/hooks/useWindowSize'; +import { contexts } from '~/shared/analytics/constants'; +import { GalleryElementTrackingProps } from '~/shared/contexts/AnalyticsContext'; import { Chain } from '~/shared/utils/chains'; import breakpoints from '../core/breakpoints'; @@ -28,6 +30,7 @@ type Props = { hasSearchKeyword: boolean; handleRefresh: () => void; onSelectToken: (tokenId: string) => void; + eventFlow?: GalleryElementTrackingProps['eventFlow']; }; const COLUMN_COUNT_DESKTOP = 4; const COLUMN_COUNT_MOBILE = 3; @@ -35,11 +38,12 @@ const COLUMN_COUNT_MOBILE = 3; export function NftSelectorView({ selectedContractAddress, onSelectContract, + onSelectToken, tokenRefs, selectedNetworkView, hasSearchKeyword, handleRefresh, - onSelectToken, + eventFlow, }: Props) { const tokens = useFragment( graphql` @@ -92,6 +96,7 @@ export function NftSelectorView({ groupOfPoapTokens.tokens.forEach((token) => { selectedCollectionTokens.push({ + dbid: groupOfPoapTokens.dbid, title: groupOfPoapTokens.title, address: groupOfPoapTokens.address, tokens: [token], @@ -110,12 +115,12 @@ export function NftSelectorView({ groupOfTokens.tokens.forEach((token) => { selectedCollectionTokens.push({ + dbid: groupOfTokens.dbid, title: groupOfTokens.title, address: groupOfTokens.address, tokens: [token], }); }); - tokens = selectedCollectionTokens; } } @@ -172,7 +177,14 @@ export function NftSelectorView({ No NFTs found, try another wallet? - diff --git a/apps/web/src/components/NftSelector/groupNftSelectorCollectionsByAddress.ts b/apps/web/src/components/NftSelector/groupNftSelectorCollectionsByAddress.ts index 1d520c48bd..725982e7fc 100644 --- a/apps/web/src/components/NftSelector/groupNftSelectorCollectionsByAddress.ts +++ b/apps/web/src/components/NftSelector/groupNftSelectorCollectionsByAddress.ts @@ -9,6 +9,7 @@ import { NftSelectorViewFragment$data } from '~/generated/NftSelectorViewFragmen export type NftSelectorCollectionGroup = { title: string; address: string; + dbid: string; tokens: Array; }; @@ -36,6 +37,7 @@ export function groupNftSelectorCollectionsByAddress({ isSpamByProvider isSpamByUser contract { + dbid chain name contractAddress { @@ -67,6 +69,7 @@ export function groupNftSelectorCollectionsByAddress({ title, tokens: [], address: token.contract.contractAddress.address, + dbid: token.contract.dbid, }; map[address] = group; diff --git a/apps/web/src/components/Notifications/CollectionLink.tsx b/apps/web/src/components/Notifications/CollectionLink.tsx index c62740bf18..68068b51d6 100644 --- a/apps/web/src/components/Notifications/CollectionLink.tsx +++ b/apps/web/src/components/Notifications/CollectionLink.tsx @@ -3,8 +3,9 @@ import { useMemo } from 'react'; import { useFragment } from 'react-relay'; import { graphql } from 'relay-runtime'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { CollectionLinkFragment$key } from '~/generated/CollectionLinkFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; type CollectionLinkProps = { collectionRef: CollectionLinkFragment$key; @@ -42,8 +43,13 @@ export function CollectionLink({ collectionRef }: CollectionLinkProps) { } return ( - + {collection.name || 'your collection'} - + ); } diff --git a/apps/web/src/components/Notifications/Notification.tsx b/apps/web/src/components/Notifications/Notification.tsx index 62a06ec6ea..b49c6cc0de 100644 --- a/apps/web/src/components/Notifications/Notification.tsx +++ b/apps/web/src/components/Notifications/Notification.tsx @@ -17,6 +17,8 @@ import { NotificationFragment$key } from '~/generated/NotificationFragment.graph import { NotificationInnerFragment$key } from '~/generated/NotificationInnerFragment.graphql'; import { NotificationInnerQueryFragment$key } from '~/generated/NotificationInnerQueryFragment.graphql'; import { NotificationQueryFragment$key } from '~/generated/NotificationQueryFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; +import { useTrack } from '~/shared/contexts/AnalyticsContext'; import { ReportingErrorBoundary } from '~/shared/errors/ReportingErrorBoundary'; import { useClearNotifications } from '~/shared/relay/useClearNotifications'; import colors from '~/shared/theme/colors'; @@ -208,8 +210,19 @@ export function Notification({ notificationRef, queryRef, toggleSubView }: Notif ]); const isClickable = Boolean(handleNotificationClick); + + const track = useTrack(); + const handleClick = useCallback(() => { - handleNotificationClick?.handleClick(); + if (handleNotificationClick?.handleClick) { + track('Notification Click', { + id: 'Notification Row', + variant: notification.__typename, + context: contexts.Notifications, + }); + handleNotificationClick.handleClick(); + } + if (!query.viewer?.user?.dbid || !query.viewer.id) { return; } @@ -218,7 +231,14 @@ export function Notification({ notificationRef, queryRef, toggleSubView }: Notif ConnectionHandler.getConnectionID(query.viewer.id, 'NotificationsFragment_notifications'), ConnectionHandler.getConnectionID(query.viewer.id, 'StandardSidebarFragment_notifications'), ]); - }, [clearAllNotifications, handleNotificationClick, query.viewer]); + }, [ + clearAllNotifications, + handleNotificationClick, + notification.__typename, + query.viewer?.id, + query.viewer?.user?.dbid, + track, + ]); const timeAgo = getTimeSince(notification.updatedTime); diff --git a/apps/web/src/components/Notifications/NotificationEmailAlert.tsx b/apps/web/src/components/Notifications/NotificationEmailAlert.tsx index aff8841b68..502449c971 100644 --- a/apps/web/src/components/Notifications/NotificationEmailAlert.tsx +++ b/apps/web/src/components/Notifications/NotificationEmailAlert.tsx @@ -10,7 +10,7 @@ import CloseIcon from '~/icons/CloseIcon'; import InfoCircleIcon from '~/icons/InfoCircleIcon'; import colors from '~/shared/theme/colors'; -import InteractiveLink from '../core/InteractiveLink/InteractiveLink'; +import GalleryLink from '../core/GalleryLink/GalleryLink'; import { HStack } from '../core/Spacer/Stack'; import { BaseM } from '../core/Text/Text'; @@ -47,7 +47,7 @@ export function NotificationEmailAlert({ onDismiss, queryRef }: Props) { Never miss a moment! Enable email notifications in settings. - Enable + Enable } /> diff --git a/apps/web/src/components/Notifications/NotificationTwitterAlert.tsx b/apps/web/src/components/Notifications/NotificationTwitterAlert.tsx index a337812be5..dc993044bc 100644 --- a/apps/web/src/components/Notifications/NotificationTwitterAlert.tsx +++ b/apps/web/src/components/Notifications/NotificationTwitterAlert.tsx @@ -8,10 +8,11 @@ import { TWITTER_AUTH_URL, TWITTER_LOCAL_STORAGE_KEY } from '~/constants/twitter import { NotificationTwitterAlertFragment$key } from '~/generated/NotificationTwitterAlertFragment.graphql'; import CloseIcon from '~/icons/CloseIcon'; import InfoCircleIcon from '~/icons/InfoCircleIcon'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import useExperience from '~/utils/graphql/experiences/useExperience'; -import InteractiveLink from '../core/InteractiveLink/InteractiveLink'; +import GalleryLink from '../core/GalleryLink/GalleryLink'; import { HStack } from '../core/Spacer/Stack'; import { BaseM } from '../core/Text/Text'; @@ -73,9 +74,16 @@ export function NotificationTwitterAlert({ queryRef }: Props) { Connect Twitter to find friends and display your handle. - + Connect - + } /> diff --git a/apps/web/src/components/Notifications/UserListItem.tsx b/apps/web/src/components/Notifications/UserListItem.tsx index 71ecd96826..c7af0336cd 100644 --- a/apps/web/src/components/Notifications/UserListItem.tsx +++ b/apps/web/src/components/Notifications/UserListItem.tsx @@ -1,5 +1,4 @@ -import Link from 'next/link'; -import { Route, route } from 'nextjs-routes'; +import { Route } from 'nextjs-routes'; import { useCallback } from 'react'; import { useFragment } from 'react-relay'; import { graphql } from 'relay-runtime'; @@ -10,8 +9,11 @@ import { VStack } from '~/components/core/Spacer/Stack'; import { BaseM, TitleDiatypeM } from '~/components/core/Text/Text'; import { useDrawerActions } from '~/contexts/globalLayout/GlobalSidebar/SidebarDrawerContext'; import { UserListItemFragment$key } from '~/generated/UserListItemFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; +import GalleryLink from '../core/GalleryLink/GalleryLink'; + type UserListItemProps = { userRef: UserListItemFragment$key; }; @@ -36,14 +38,22 @@ export function UserListItem({ userRef }: UserListItemProps) { }, [hideDrawer]); return ( - - - - {user.username} - {user.bio && {user.bio && }} - - - + + + {user.username} + {user.bio && ( + + {user.bio && } + + )} + + ); } @@ -58,10 +68,6 @@ const BioText = styled(BaseM)` } `; -const StyledLink = styled.a` - text-decoration: none; -`; - const Container = styled(VStack)` padding: 16px 12px; diff --git a/apps/web/src/components/Notifications/notifications/NewTokens.tsx b/apps/web/src/components/Notifications/notifications/NewTokens.tsx index 4513d09773..9388cf61d9 100644 --- a/apps/web/src/components/Notifications/notifications/NewTokens.tsx +++ b/apps/web/src/components/Notifications/notifications/NewTokens.tsx @@ -11,7 +11,7 @@ import { useModalActions } from '~/contexts/modal/ModalContext'; import { NewTokensFragment$key } from '~/generated/NewTokensFragment.graphql'; import { NewTokensTokenPreviewFragment$key } from '~/generated/NewTokensTokenPreviewFragment.graphql'; import { useIsMobileWindowWidth } from '~/hooks/useWindowSize'; -import { useTrack } from '~/shared/contexts/AnalyticsContext'; +import { contexts, flows } from '~/shared/analytics/constants'; import { useGetSinglePreviewImage } from '~/shared/relay/useGetPreviewImages'; import colors from '~/shared/theme/colors'; @@ -41,7 +41,6 @@ export function NewTokens({ notificationRef, onClose }: Props) { const { token } = notification; const isMobile = useIsMobileWindowWidth(); - const track = useTrack(); const { showModal } = useModalActions(); if (token?.__typename !== 'Token') { @@ -51,14 +50,18 @@ export function NewTokens({ notificationRef, onClose }: Props) { const quantity = notification.count ?? 1; const handleCreatePostClick = useCallback(() => { - track('NFT Detail: Clicked Create Post'); showModal({ - content: , + content: ( + + ), headerVariant: 'thicc', isFullPage: isMobile, }); onClose(); - }, [isMobile, onClose, showModal, token, track]); + }, [isMobile, onClose, showModal, token]); return ( @@ -79,7 +82,13 @@ export function NewTokens({ notificationRef, onClose }: Props) { - + Post diff --git a/apps/web/src/components/Notifications/notifications/NotificationPostPreview.tsx b/apps/web/src/components/Notifications/notifications/NotificationPostPreview.tsx index c1f919710e..b85b912639 100644 --- a/apps/web/src/components/Notifications/notifications/NotificationPostPreview.tsx +++ b/apps/web/src/components/Notifications/notifications/NotificationPostPreview.tsx @@ -28,7 +28,8 @@ function NotificationPostPreview({ tokenRef }: NotificationPostPreviewProps) { const StyledPostPreview = styled.img` height: 100%; - width: 100%; + width: 56px; + object-fit: cover; `; type NotificationPostPreviewWithBoundaryProps = { diff --git a/apps/web/src/components/Notifications/notifications/SomeoneCommentedOnYourPost.tsx b/apps/web/src/components/Notifications/notifications/SomeoneCommentedOnYourPost.tsx index 1c8f608c4f..1ccd30a5cd 100644 --- a/apps/web/src/components/Notifications/notifications/SomeoneCommentedOnYourPost.tsx +++ b/apps/web/src/components/Notifications/notifications/SomeoneCommentedOnYourPost.tsx @@ -88,6 +88,7 @@ const StyledCaption = styled(BaseM)` font-size: 12px; border-left: 2px solid ${colors.porcelain}; padding-left: 8px; + word-break: break-word; display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; diff --git a/apps/web/src/components/Onboarding/OnboardingFooter.tsx b/apps/web/src/components/Onboarding/OnboardingFooter.tsx index d5152541c8..a076b98641 100644 --- a/apps/web/src/components/Onboarding/OnboardingFooter.tsx +++ b/apps/web/src/components/Onboarding/OnboardingFooter.tsx @@ -9,7 +9,7 @@ type Props = { step: StepName; onNext: () => void | Promise; isNextEnabled: boolean; - onPrevious: () => void; + onPrevious?: () => void; previousTextOverride?: string; }; @@ -27,9 +27,10 @@ export function OnboardingFooter({ return ( diff --git a/apps/web/src/components/Pill.tsx b/apps/web/src/components/Pill.tsx deleted file mode 100644 index a2849888ec..0000000000 --- a/apps/web/src/components/Pill.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import styled, { css } from 'styled-components'; - -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; -import colors from '~/shared/theme/colors'; - -export const ClickablePill = styled(InteractiveLink)<{ active?: boolean; className?: string }>` - border: 1px solid ${colors.porcelain}; - padding: 0 12px; - border-radius: 24px; - color: ${colors.black['800']}; - text-decoration: none; - width: fit-content; - max-width: 100%; - align-self: end; - height: 32px; - display: flex; - align-items: center; - - ${({ active }) => - active && - css` - border-color: ${colors.black['800']}; - `} - - &:hover { - border-color: ${colors.black['800']}; - } -`; - -export const ButtonPill = styled.button<{ active?: boolean }>` - border: 1px solid ${colors.porcelain}; - padding: 0 12px; - border-radius: 24px; - color: ${colors.shadow}; - text-decoration: none; - width: fit-content; - max-width: 100%; - align-self: end; - height: 32px; - display: flex; - align-items: center; - background-color: ${colors.offWhite}; - cursor: pointer; - - ${({ active }) => - active && - css` - border-color: ${colors.black['800']}; - `} - - &:hover { - border-color: ${colors.black['800']}; - background-color: ${colors.faint}; - } -`; - -export const NonclickablePill = styled.div` - color: ${colors.black['800']}; - width: fit-content; - max-width: 100%; - align-self: end; - height: 32px; - display: flex; - align-items: center; -`; diff --git a/apps/web/src/components/Posts/DiscardPostConfirmation.tsx b/apps/web/src/components/Posts/DiscardPostConfirmation.tsx index 238c1d3876..b335421069 100644 --- a/apps/web/src/components/Posts/DiscardPostConfirmation.tsx +++ b/apps/web/src/components/Posts/DiscardPostConfirmation.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import { useModalActions } from '~/contexts/modal/ModalContext'; import { EditPencilIcon } from '~/icons/EditPencilIcon'; import { TrashIconNew } from '~/icons/TrashIconNew'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import { Button } from '../core/Button/Button'; @@ -32,12 +33,23 @@ export default function DiscardPostConfirmation({ onSaveDraft, onDiscard }: Prop If you go back now, this post will be discarded. - + SAVE DRAFT - + DISCARD diff --git a/apps/web/src/components/Posts/PostComposer.tsx b/apps/web/src/components/Posts/PostComposer.tsx index 8a49c7e86d..1fed661f1e 100644 --- a/apps/web/src/components/Posts/PostComposer.tsx +++ b/apps/web/src/components/Posts/PostComposer.tsx @@ -11,7 +11,8 @@ import { PostComposerTokenFragment$key } from '~/generated/PostComposerTokenFrag import useCreatePost from '~/hooks/api/posts/useCreatePost'; import AlertTriangleIcon from '~/icons/AlertTriangleIcon'; import { ChevronLeftIcon } from '~/icons/ChevronLeftIcon'; -import { useTrack } from '~/shared/contexts/AnalyticsContext'; +import { contexts } from '~/shared/analytics/constants'; +import { GalleryElementTrackingProps, useTrack } from '~/shared/contexts/AnalyticsContext'; import { useReportError } from '~/shared/contexts/ErrorReportingContext'; import colors from '~/shared/theme/colors'; @@ -20,17 +21,18 @@ import { Button } from '../core/Button/Button'; import IconContainer from '../core/IconContainer'; import { HStack, VStack } from '../core/Spacer/Stack'; import { TitleS } from '../core/Text/Text'; -import { TextAreaWithCharCount } from '../core/TextArea/TextArea'; +import { AutoResizingTextAreaWithCharCount } from '../core/TextArea/TextArea'; import PostComposerNft from './PostComposerNft'; type Props = { tokenId: string; onBackClick?: () => void; + eventFlow?: GalleryElementTrackingProps['eventFlow']; }; const DESCRIPTION_MAX_LENGTH = 600; -export default function PostComposer({ onBackClick, tokenId }: Props) { +export default function PostComposer({ onBackClick, tokenId, eventFlow }: Props) { const query = useLazyLoadQuery( graphql` query PostComposerQuery($tokenId: DBID!) { @@ -86,6 +88,8 @@ export default function PostComposer({ onBackClick, tokenId }: Props) { const handlePostClick = useCallback(async () => { setIsSubmitting(true); track('Clicked Post in Post Composer', { + context: contexts.Posts, + flow: eventFlow, added_description: Boolean(captionRef.current), }); try { @@ -108,6 +112,7 @@ export default function PostComposer({ onBackClick, tokenId }: Props) { } }, [ track, + eventFlow, captionRef, createPost, token.dbid, @@ -147,7 +152,7 @@ export default function PostComposer({ onBackClick, tokenId }: Props) { - - + {generalError && ( @@ -168,13 +173,17 @@ export default function PostComposer({ onBackClick, tokenId }: Props) { )} - + ); } @@ -188,6 +197,10 @@ const StyledPostComposer = styled(VStack)` } `; +const StyledHStack = styled(HStack)` + padding-top: 16px; +`; + const StyledHeader = styled(HStack)` padding-top: 16px; `; diff --git a/apps/web/src/components/Posts/PostComposerAsset.tsx b/apps/web/src/components/Posts/PostComposerAsset.tsx index 8d0c2207f3..2744e33516 100644 --- a/apps/web/src/components/Posts/PostComposerAsset.tsx +++ b/apps/web/src/components/Posts/PostComposerAsset.tsx @@ -3,7 +3,6 @@ import styled from 'styled-components'; import { PostComposerAssetFragment$key } from '~/generated/PostComposerAssetFragment.graphql'; -import breakpoints from '../core/breakpoints'; import { NftFailureBoundary } from '../NftFailureFallback/NftFailureBoundary'; import { NftSelectorPreviewAsset } from '../NftSelector/NftSelectorPreviewAsset'; @@ -26,22 +25,18 @@ export default function PostComposerAsset({ tokenRef }: Props) { return ( - + ); } const StyledPostComposerAsset = styled.div` + display: flex; + justify-content: center; + align-items: center; width: 100%; height: 100%; min-width: 100%; min-height: 100%; - - @media only screen and ${breakpoints.tablet} { - width: 180px; - height: 180px; - min-width: 180px; - min-height: 180px; - } `; diff --git a/apps/web/src/components/Posts/PostComposerModal.tsx b/apps/web/src/components/Posts/PostComposerModal.tsx index 3fac22b78b..23ac0ca5b3 100644 --- a/apps/web/src/components/Posts/PostComposerModal.tsx +++ b/apps/web/src/components/Posts/PostComposerModal.tsx @@ -5,6 +5,8 @@ import { NftSelectorLoadingView } from '~/components/NftSelector/NftSelectorLoad import ErrorBoundary from '~/contexts/boundary/ErrorBoundary'; import { useModalActions } from '~/contexts/modal/ModalContext'; import { usePostComposerContext } from '~/contexts/postComposer/PostComposerContext'; +import { contexts } from '~/shared/analytics/constants'; +import { GalleryElementTrackingProps, useTrack } from '~/shared/contexts/AnalyticsContext'; import { useClearURLQueryParams } from '~/utils/useClearURLQueryParams'; import breakpoints from '../core/breakpoints'; @@ -17,15 +19,25 @@ import PostComposer from './PostComposer'; type Props = { preSelectedContract?: NftSelectorContractType; + eventFlow?: GalleryElementTrackingProps['eventFlow']; }; // Modal with multiple steps: the NFT Selector -> then Post Composer -export function PostComposerModalWithSelector({ preSelectedContract }: Props) { +export function PostComposerModalWithSelector({ preSelectedContract, eventFlow }: Props) { const [selectedTokenId, setSelectedTokenId] = useState(null); - const onSelectToken = useCallback((tokenId: string) => { - setSelectedTokenId(tokenId); - }, []); + const track = useTrack(); + + const onSelectToken = useCallback( + (tokenId: string) => { + track('Select Token on Post Composer Modal', { + context: contexts.Posts, + flow: eventFlow, + }); + setSelectedTokenId(tokenId); + }, + [eventFlow, track] + ); const returnUserToSelectorStep = useCallback(() => { setSelectedTokenId(null); @@ -38,6 +50,11 @@ export function PostComposerModalWithSelector({ preSelectedContract }: Props) { const { captionRef, setCaption } = usePostComposerContext(); const onBackClick = useCallback(() => { + track('Back Click on Post Composer Modal', { + context: contexts.Posts, + flow: eventFlow, + }); + if (!captionRef.current) { returnUserToSelectorStep(); return; @@ -58,7 +75,7 @@ export function PostComposerModalWithSelector({ preSelectedContract }: Props) { ), isFullPage: false, }); - }, [captionRef, showModal, returnUserToSelectorStep, setCaption]); + }, [track, eventFlow, captionRef, showModal, returnUserToSelectorStep, setCaption]); return ( @@ -66,13 +83,18 @@ export function PostComposerModalWithSelector({ preSelectedContract }: Props) { {selectedTokenId ? ( // Just in case the PostComposer's token isn't already in the cache, we'll have a fallback }> - + ) : ( )} @@ -82,15 +104,16 @@ export function PostComposerModalWithSelector({ preSelectedContract }: Props) { type PostComposerModalProps = { tokenId: string; + eventFlow?: GalleryElementTrackingProps['eventFlow']; }; // Modal with a single step, the Post Composer. -export function PostComposerModal({ tokenId }: PostComposerModalProps) { +export function PostComposerModal({ tokenId, eventFlow }: PostComposerModalProps) { useClearURLQueryParams('composer'); return ( }> - + ); @@ -101,7 +124,7 @@ const StyledPostComposerModal = styled.div` height: 100%; @media only screen and ${breakpoints.tablet} { min-width: 562px; - min-height: 344px; + min-height: 274px; } `; @@ -118,7 +141,14 @@ function PostComposerErrorScreen() { Sorry, there was an error while composing your post. The Gallery team has been notified. - Close + + Close + ); diff --git a/apps/web/src/components/ProcessedText/ProcessedText.tsx b/apps/web/src/components/ProcessedText/ProcessedText.tsx new file mode 100644 index 0000000000..9f2b64028a --- /dev/null +++ b/apps/web/src/components/ProcessedText/ProcessedText.tsx @@ -0,0 +1,43 @@ +import { graphql, useFragment } from 'react-relay'; + +import { ProcessedTextFragment$key } from '~/generated/ProcessedTextFragment.graphql'; +import GalleryProcessedText from '~/shared/components/GalleryProccessedText/GalleryProcessedText'; +import { GalleryElementTrackingProps } from '~/shared/contexts/AnalyticsContext'; + +import { LinkComponent } from './elements/LinkComponent'; +import { MentionComponent } from './elements/MentionComponent'; +import { TextComponent } from './elements/TextComponent'; + +type ProcessedTextProps = { + text: string; + mentionsRef?: ProcessedTextFragment$key; + eventContext: GalleryElementTrackingProps['eventContext']; +}; + +export default function ProcessedText({ + text, + mentionsRef = [], + eventContext, +}: ProcessedTextProps) { + const mentions = useFragment( + graphql` + fragment ProcessedTextFragment on Mention @relay(plural: true) { + __typename + ...GalleryProcessedTextFragment + ...MentionComponentFragment + } + `, + mentionsRef + ); + + return ( + } + MentionComponent={(props) => } + BreakComponent={() =>
} + /> + ); +} diff --git a/apps/web/src/components/ProcessedText/elements/LinkComponent.tsx b/apps/web/src/components/ProcessedText/elements/LinkComponent.tsx new file mode 100644 index 0000000000..5bb25f4dc6 --- /dev/null +++ b/apps/web/src/components/ProcessedText/elements/LinkComponent.tsx @@ -0,0 +1,35 @@ +import GalleryLink, { + GalleryLinkNeedsVerification, +} from '~/components/core/GalleryLink/GalleryLink'; +import { GalleryElementTrackingProps } from '~/shared/contexts/AnalyticsContext'; + +type LinkComponentProps = { + value?: string; + url: string; + eventContext: GalleryElementTrackingProps['eventContext']; +}; + +export function LinkComponent({ url, value, eventContext }: LinkComponentProps) { + const isInternalLink = url.startsWith('https://gallery.so/'); + + if (isInternalLink) { + return ( + + {value ?? url} + + ); + } + return ( + + {value ?? url} + + ); +} diff --git a/apps/web/src/components/ProcessedText/elements/MentionComponent.tsx b/apps/web/src/components/ProcessedText/elements/MentionComponent.tsx new file mode 100644 index 0000000000..1bf6186c63 --- /dev/null +++ b/apps/web/src/components/ProcessedText/elements/MentionComponent.tsx @@ -0,0 +1,79 @@ +import { graphql, useFragment } from 'react-relay'; +import styled from 'styled-components'; + +import CommunityHoverCard from '~/components/HoverCard/CommunityHoverCard'; +import UserHoverCard from '~/components/HoverCard/UserHoverCard'; +import { GalleryTextElementParserMentionsFragment$data } from '~/generated/GalleryTextElementParserMentionsFragment.graphql'; +import { MentionComponentFragment$key } from '~/generated/MentionComponentFragment.graphql'; + +type Props = { + mention: string; + mentionData: GalleryTextElementParserMentionsFragment$data['entity']; + mentionsRef: MentionComponentFragment$key; +}; + +export function MentionComponent({ mention, mentionData, mentionsRef }: Props) { + const query = useFragment( + graphql` + fragment MentionComponentFragment on Mention @relay(plural: true) { + __typename + ...GalleryTextElementParserMentionsFragment + entity { + __typename + ... on GalleryUser { + __typename + username + ...UserHoverCardFragment + } + ... on Community { + __typename + name + contractAddress { + address + } + ...CommunityHoverCardFragment + } + } + } + `, + mentionsRef + ); + + if (!mentionData) return null; + + if (mentionData.__typename === 'GalleryUser' && mentionData.username) { + const user = query.find( + (mention) => + mention.entity?.__typename === 'GalleryUser' && + mention.entity.username === mentionData.username + )?.entity; + + if (user?.__typename !== 'GalleryUser') return null; + + return ( + + {mention} + + ); + } + + if (mentionData.__typename === 'Community') { + const community = query.find( + (mention) => + mention.entity?.__typename === 'Community' && + mention.entity.contractAddress?.address === mentionData.contractAddress?.address + )?.entity; + + if (community?.__typename !== 'Community') return null; + + return ( + + {mention} + + ); + } + + return {mention}; +} + +const StyledMentionText = styled.span``; diff --git a/apps/web/src/components/ProcessedText/elements/TextComponent.tsx b/apps/web/src/components/ProcessedText/elements/TextComponent.tsx new file mode 100644 index 0000000000..06080f693b --- /dev/null +++ b/apps/web/src/components/ProcessedText/elements/TextComponent.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from 'react'; + +type Props = { + children: ReactNode; +}; + +export function TextComponent({ children, ...props }: Props) { + return {children}; +} diff --git a/apps/web/src/components/Profile/EditUserInfoForm.tsx b/apps/web/src/components/Profile/EditUserInfoForm.tsx index 6f8d6f7ec8..befae4ede2 100644 --- a/apps/web/src/components/Profile/EditUserInfoForm.tsx +++ b/apps/web/src/components/Profile/EditUserInfoForm.tsx @@ -32,7 +32,7 @@ type Props = { export const BIO_MAX_CHAR_COUNT = 600; -function UserInfoForm({ +function EditUserInfoForm({ className, onSubmit, username, @@ -145,7 +145,7 @@ function UserInfoForm({ {!mode && ( - + - + {ensProfileImage ? ( @@ -118,19 +122,24 @@ export function ProfilePictureDropdown({ open, onClose, queryRef }: Props) { Use ENS Avatar - + Choose from collection {user.profileImage && ( - - - - Remove current profile picture - - + )} diff --git a/apps/web/src/components/ProfilePicture/ProfilePicture.tsx b/apps/web/src/components/ProfilePicture/ProfilePicture.tsx index a77e92950c..1e50328768 100644 --- a/apps/web/src/components/ProfilePicture/ProfilePicture.tsx +++ b/apps/web/src/components/ProfilePicture/ProfilePicture.tsx @@ -1,21 +1,35 @@ -import Link from 'next/link'; import { Route } from 'nextjs-routes'; -import { useMemo } from 'react'; +import { PropsWithChildren, useMemo } from 'react'; import { graphql, useFragment } from 'react-relay'; -import styled from 'styled-components'; import { ProfilePictureFragment$key } from '~/generated/ProfilePictureFragment.graphql'; import { ProfilePictureValidFragment$key } from '~/generated/ProfilePictureValidFragment.graphql'; import { useGetSinglePreviewImage } from '~/shared/relay/useGetPreviewImages'; +import GalleryLink from '../core/GalleryLink/GalleryLink'; import { NftFailureBoundary } from '../NftFailureFallback/NftFailureBoundary'; import { RawProfilePicture, RawProfilePictureProps } from './RawProfilePicture'; type Props = { userRef: ProfilePictureFragment$key; + clickDisabled?: boolean; } & Omit; -export function ProfilePicture({ userRef, ...rest }: Props) { +function PfpLinkWrapper({ userRoute, children }: PropsWithChildren<{ userRoute: Route }>) { + return ( + + {children} + + ); +} + +export function ProfilePicture({ userRef, clickDisabled = false, ...rest }: Props) { const user = useFragment( graphql` fragment ProfilePictureFragment on GalleryUser { @@ -46,30 +60,18 @@ export function ProfilePicture({ userRef, ...rest }: Props) { return { pathname: '/[username]', query: { username: user.username as string } }; }, [user]); - if (!user) return null; - - const { token, profileImage } = user.profileImage ?? {}; + const { token, profileImage } = user?.profileImage ?? {}; const firstLetter = user?.username?.substring(0, 1) ?? ''; - if (profileImage && profileImage.previewURLs?.medium) { + const renderedPfp = useMemo(() => { + if (profileImage?.previewURLs?.medium) { + return ; + } + if (!token) { + return ; + } return ( - - - - ); - } - - if (!token) { - return ( - - - - ); - } - - return ( - } @@ -77,8 +79,18 @@ export function ProfilePicture({ userRef, ...rest }: Props) { > - - ); + ); + }, [firstLetter, profileImage, rest, token]); + + if (!user) { + return null; + } + + if (clickDisabled) { + return renderedPfp; + } + + return {renderedPfp}; } type ValidProfilePictureProps = { @@ -99,8 +111,3 @@ function ValidProfilePicture({ tokenRef, ...rest }: ValidProfilePictureProps) { return ; } - -const StyledLink = styled(Link)` - text-decoration: none; - cursor: pointer; -`; diff --git a/apps/web/src/components/Search/SearchFilter.tsx b/apps/web/src/components/Search/SearchFilter.tsx index 90577fef13..a5b8f29534 100644 --- a/apps/web/src/components/Search/SearchFilter.tsx +++ b/apps/web/src/components/Search/SearchFilter.tsx @@ -1,11 +1,12 @@ import { useCallback } from 'react'; import styled from 'styled-components'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import { HStack } from '../core/Spacer/Stack'; import { TitleDiatypeM } from '../core/Text/Text'; -import { ButtonPill } from '../Pill'; +import { GalleryPill } from '../GalleryPill'; import { SearchFilterType } from './Search'; type Props = { @@ -49,6 +50,9 @@ export default function SearchFilter({ activeFilter, onChangeFilter }: Props) { {filters.map((filter) => ( handleFilterChange(filter.value)} @@ -64,8 +68,9 @@ const StyledFilterContainer = styled(HStack)` padding: 8px 16px; `; -const StyledButtonPill = styled(ButtonPill)<{ active?: boolean }>` - cursor: pointer; +const StyledButtonPill = styled(GalleryPill)<{ active?: boolean }>` + background: transparent; + ${TitleDiatypeM} { color: ${({ active }) => (active ? colors.black['800'] : colors.shadow)}; } diff --git a/apps/web/src/components/Search/SearchResult.tsx b/apps/web/src/components/Search/SearchResult.tsx index 99916bc180..55d6b3875d 100644 --- a/apps/web/src/components/Search/SearchResult.tsx +++ b/apps/web/src/components/Search/SearchResult.tsx @@ -1,12 +1,14 @@ -import Link, { LinkProps } from 'next/link'; import { Route, route } from 'nextjs-routes'; import { ReactNode, useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { useDrawerActions } from '~/contexts/globalLayout/GlobalSidebar/SidebarDrawerContext'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import colors from '~/shared/theme/colors'; +import { getHighlightedDescription, getHighlightedName } from '~/shared/utils/highlighter'; +import GalleryLink from '../core/GalleryLink/GalleryLink'; import Markdown from '../core/Markdown/Markdown'; import { HStack, VStack } from '../core/Spacer/Stack'; import { BaseM } from '../core/Text/Text'; @@ -21,8 +23,6 @@ type Props = { profilePicture?: ReactNode; }; -const MAX_DESCRIPTION_CHARACTER = 150; - export default function SearchResult({ name, description, @@ -45,49 +45,29 @@ export default function SearchResult({ searchQuery: keyword, pathname: fullLink, resultType: type, + context: contexts.Search, }); }, [hideDrawer, keyword, path, track, type]); - const highlightedName = useMemo(() => { - return name.replace(new RegExp(keyword, 'gi'), (match) => `**${match}**`); - }, [keyword, name]); - - const highlightedDescription = useMemo(() => { - const regex = new RegExp(keyword, 'gi'); - - // Remove bold & link markdown tag from description - const unformattedDescription = description.replace(/\*\*/g, '').replace(/\[.*\]\(.*\)/g, ''); + const highlightedName = useMemo(() => getHighlightedName(name, keyword), [keyword, name]); - const matchIndex = unformattedDescription.search(regex); - let truncatedDescription; - - const maxLength = MAX_DESCRIPTION_CHARACTER; - - if (matchIndex > -1 && matchIndex + keyword.length === unformattedDescription.length) { - const endIndex = Math.min(unformattedDescription.length, maxLength); - truncatedDescription = `...${unformattedDescription.substring( - endIndex - maxLength, - endIndex - )}`; - } else { - truncatedDescription = unformattedDescription.substring(0, maxLength); - } - // highlight keyword - return truncatedDescription.replace(regex, (match) => `**${match}**`); - }, [keyword, description]); + const highlightedDescription = useMemo( + () => getHighlightedDescription(description, keyword), + [keyword, description] + ); return ( - + {profilePicture} - + {highlightedDescription && ( - + )} @@ -97,7 +77,7 @@ export default function SearchResult({ ); } -const StyledSearchResult = styled(Link)` +const StyledSearchResult = styled(GalleryLink)<{ className: string }>` color: ${colors.black['800']}; padding: 16px 12px; cursor: pointer; diff --git a/apps/web/src/components/Search/SearchSection.tsx b/apps/web/src/components/Search/SearchSection.tsx index 3da34fd3c9..b2f8eed407 100644 --- a/apps/web/src/components/Search/SearchSection.tsx +++ b/apps/web/src/components/Search/SearchSection.tsx @@ -1,8 +1,9 @@ import styled from 'styled-components'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; -import InteractiveLink from '../core/InteractiveLink/InteractiveLink'; +import GalleryLink from '../core/GalleryLink/GalleryLink'; import { HStack, VStack } from '../core/Spacer/Stack'; import { TitleDiatypeL, TitleXS } from '../core/Text/Text'; import { NUM_PREVIEW_SEARCH_RESULTS } from './constants'; @@ -37,7 +38,14 @@ export default function SearchSection({ {title} {!isShowAll && numResults > NUM_PREVIEW_SEARCH_RESULTS && ( - Show all + + Show all + )} {children} @@ -54,7 +62,7 @@ const StyledResultHeader = styled(HStack)` padding: 0 12px; `; -const StyledInteractiveLink = styled(InteractiveLink)` +const StyledGalleryLink = styled(GalleryLink)` font-size: 12px; line-height: 16px; `; diff --git a/apps/web/src/components/Settings/ManageEmailSection/ManageEmailSection.tsx b/apps/web/src/components/Settings/ManageEmailSection/ManageEmailSection.tsx index 573cfc311d..d8a67e881c 100644 --- a/apps/web/src/components/Settings/ManageEmailSection/ManageEmailSection.tsx +++ b/apps/web/src/components/Settings/ManageEmailSection/ManageEmailSection.tsx @@ -10,6 +10,7 @@ import EmailManager from '~/components/Email/EmailManager'; import useUpdateEmailNotificationSettings from '~/components/Email/useUpdateEmailNotificationSettings'; import { useToastActions } from '~/contexts/toast/ToastContext'; import { ManageEmailSectionFragment$key } from '~/generated/ManageEmailSectionFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; import { useReportError } from '~/shared/contexts/ErrorReportingContext'; import colors from '~/shared/theme/colors'; @@ -149,10 +150,16 @@ export default function ManageEmailSection({ queryRef }: Props) { - {shouldDisplayAddEmailInput && userEmail ? ( + {shouldDisplayAddEmailInput ? ( ) : ( - + add email address )} diff --git a/apps/web/src/components/Settings/MembersClubSection/MembersClubSection.tsx b/apps/web/src/components/Settings/MembersClubSection/MembersClubSection.tsx index 9712bb2198..1934cb4ddb 100644 --- a/apps/web/src/components/Settings/MembersClubSection/MembersClubSection.tsx +++ b/apps/web/src/components/Settings/MembersClubSection/MembersClubSection.tsx @@ -1,12 +1,13 @@ import { useMemo } from 'react'; import { graphql, useFragment } from 'react-relay'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { BaseM, TitleDiatypeL } from '~/components/core/Text/Text'; import { GALLERY_DISCORD } from '~/constants/urls'; import { MembersClubSectionFragment$key } from '~/generated/MembersClubSectionFragment.graphql'; import CircleCheckIcon from '~/icons/CircleCheckIcon'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import { GALLERY_OS_ADDRESS } from '~/shared/utils/getOpenseaExternalUrl'; @@ -42,12 +43,23 @@ export default function MembersClubSection({ queryRef }: Props) { Unlock early access to features, a profile badge, and the members-only{' '} - Discord channel by holding a{' '} - + Discord channel + {' '} + by holding a{' '} + Premium Gallery Membership Card - {' '} + {' '} and verifying your email address. diff --git a/apps/web/src/components/Settings/MobileAuthManagerSection/MobileAuthManagerSection.tsx b/apps/web/src/components/Settings/MobileAuthManagerSection/MobileAuthManagerSection.tsx index dd35dcea65..5155d58459 100644 --- a/apps/web/src/components/Settings/MobileAuthManagerSection/MobileAuthManagerSection.tsx +++ b/apps/web/src/components/Settings/MobileAuthManagerSection/MobileAuthManagerSection.tsx @@ -2,11 +2,12 @@ import { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; import { Button } from '~/components/core/Button/Button'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { BaseM, TitleDiatypeL } from '~/components/core/Text/Text'; import QRCode from '~/components/QRCode/QRCode'; import { useModalActions } from '~/contexts/modal/ModalContext'; +import { contexts, flows } from '~/shared/analytics/constants'; import { useReportError } from '~/shared/contexts/ErrorReportingContext'; import SettingsRowDescription from '../SettingsRowDescription'; @@ -31,8 +32,23 @@ export default function MobileAuthManagerSection() { - - Learn more about the app + + + Learn more about the app + ); diff --git a/apps/web/src/components/Settings/Settings.tsx b/apps/web/src/components/Settings/Settings.tsx index bde547bffb..92d8926345 100644 --- a/apps/web/src/components/Settings/Settings.tsx +++ b/apps/web/src/components/Settings/Settings.tsx @@ -7,6 +7,7 @@ import { Button } from '~/components/core/Button/Button'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { SettingsFragment$key } from '~/generated/SettingsFragment.graphql'; import { useLogout } from '~/hooks/useLogout'; +import { contexts, flows } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import { useClearURLQueryParams } from '~/utils/useClearURLQueryParams'; @@ -70,7 +71,14 @@ function Settings({ queryRef, onLogout, header }: Props) { - + Sign Out diff --git a/apps/web/src/components/TezosDomainOrAddress.tsx b/apps/web/src/components/TezosDomainOrAddress.tsx index 9d16e3360d..354f0022b9 100644 --- a/apps/web/src/components/TezosDomainOrAddress.tsx +++ b/apps/web/src/components/TezosDomainOrAddress.tsx @@ -9,6 +9,7 @@ import useSWR from 'swr'; import { LinkableAddress, RawLinkableAddress } from '~/components/LinkableAddress'; import { TezosDomainOrAddressFragment$key } from '~/generated/TezosDomainOrAddressFragment.graphql'; import { TezosDomainOrAddressWithSuspenseFragment$key } from '~/generated/TezosDomainOrAddressWithSuspenseFragment.graphql'; +import { GalleryElementTrackingProps } from '~/shared/contexts/AnalyticsContext'; import { ReportingErrorBoundary } from '~/shared/errors/ReportingErrorBoundary'; import { getExternalAddressLink } from '~/shared/utils/wallet'; @@ -22,9 +23,10 @@ async function tezosDomainFetcher(address: string): Promise { type TezosDomainProps = { chainAddressRef: TezosDomainOrAddressFragment$key; + eventContext: GalleryElementTrackingProps['eventContext']; }; -const TezosDomain = ({ chainAddressRef }: TezosDomainProps) => { +const TezosDomain = ({ chainAddressRef, eventContext }: TezosDomainProps) => { const address = useFragment( graphql` fragment TezosDomainOrAddressFragment on ChainAddress { @@ -42,18 +44,29 @@ const TezosDomain = ({ chainAddressRef }: TezosDomainProps) => { const link = getExternalAddressLink(address); if (domain && link) { - return ; + return ( + + ); } // If we couldn't resolve, let's fallback to the default component - return ; + return ; }; type TezosDomainOrAddressProps = { chainAddressRef: TezosDomainOrAddressWithSuspenseFragment$key; + eventContext: GalleryElementTrackingProps['eventContext']; }; -export const TezosDomainOrAddress = ({ chainAddressRef }: TezosDomainOrAddressProps) => { +export const TezosDomainOrAddress = ({ + chainAddressRef, + eventContext, +}: TezosDomainOrAddressProps) => { const address = useFragment( graphql` fragment TezosDomainOrAddressWithSuspenseFragment on ChainAddress { @@ -67,9 +80,11 @@ export const TezosDomainOrAddress = ({ chainAddressRef }: TezosDomainOrAddressPr ); return ( - }> - }> - + }> + } + > + ); diff --git a/apps/web/src/components/TokenHolderList/TokenHolderListItem.tsx b/apps/web/src/components/TokenHolderList/TokenHolderListItem.tsx index 855d8d2a0b..fc7f479988 100644 --- a/apps/web/src/components/TokenHolderList/TokenHolderListItem.tsx +++ b/apps/web/src/components/TokenHolderList/TokenHolderListItem.tsx @@ -5,17 +5,18 @@ import styled from 'styled-components'; import breakpoints, { size } from '~/components/core/breakpoints'; import { Directions } from '~/components/core/enums'; -import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { BaseM } from '~/components/core/Text/Text'; import { useMemberListPageActions } from '~/contexts/memberListPage/MemberListPageContext'; import { TokenHolderListItemFragment$key } from '~/generated/TokenHolderListItemFragment.graphql'; import { useBreakpoint } from '~/hooks/useWindowSize'; +import { contexts } from '~/shared/analytics/constants'; import useDebounce from '~/shared/hooks/useDebounce'; import { removeNullValues } from '~/shared/relay/removeNullValues'; import colors from '~/shared/theme/colors'; import { graphqlTruncateUniversalUsername } from '~/shared/utils/wallet'; import detectMobileDevice from '~/utils/detectMobileDevice'; +import GalleryLink from '../core/GalleryLink/GalleryLink'; import { HStack } from '../core/Spacer/Stack'; import UserHoverCard from '../HoverCard/UserHoverCard'; import { ProfilePicture } from '../ProfilePicture/ProfilePicture'; @@ -108,8 +109,10 @@ function TokenHolderListItem({ tokenHolderRef, direction, fadeUsernames }: Props {owner.user.universal ? ( {username} @@ -117,8 +120,10 @@ function TokenHolderListItem({ tokenHolderRef, direction, fadeUsernames }: Props diff --git a/apps/web/src/components/Twitter/TwitterFollowingModal.tsx b/apps/web/src/components/Twitter/TwitterFollowingModal.tsx index be785cae86..fc0f08fca0 100644 --- a/apps/web/src/components/Twitter/TwitterFollowingModal.tsx +++ b/apps/web/src/components/Twitter/TwitterFollowingModal.tsx @@ -1,4 +1,3 @@ -import Link, { LinkProps } from 'next/link'; import { useCallback, useMemo, useState } from 'react'; import { graphql, useFragment, usePaginationFragment } from 'react-relay'; import { AutoSizer, InfiniteLoader, List, ListRowRenderer } from 'react-virtualized'; @@ -11,11 +10,13 @@ import { useToastActions } from '~/contexts/toast/ToastContext'; import { TwitterFollowingModalFragment$key } from '~/generated/TwitterFollowingModalFragment.graphql'; import { TwitterFollowingModalMutation } from '~/generated/TwitterFollowingModalMutation.graphql'; import { TwitterFollowingModalQueryFragment$key } from '~/generated/TwitterFollowingModalQueryFragment.graphql'; +import { contexts, flows } from '~/shared/analytics/constants'; import { useReportError } from '~/shared/contexts/ErrorReportingContext'; import { usePromisifiedMutation } from '~/shared/relay/usePromisifiedMutation'; import breakpoints from '../core/breakpoints'; import { Button } from '../core/Button/Button'; +import GalleryLink from '../core/GalleryLink/GalleryLink'; import Markdown from '../core/Markdown/Markdown'; import { HStack, VStack } from '../core/Spacer/Stack'; import { BaseM, TitleDiatypeL } from '../core/Text/Text'; @@ -202,20 +203,23 @@ export default function TwitterFollowingModal({ followingRef, queryRef }: Props) return ( - {user.username} - + - + @@ -274,10 +278,21 @@ export default function TwitterFollowingModal({ followingRef, queryRef }: Props) - + SKIP ` - text-decoration: none; -`; - const BioText = styled(BaseM)` display: -webkit-box; -webkit-line-clamp: 1; diff --git a/apps/web/src/components/Twitter/TwitterSetting.tsx b/apps/web/src/components/Twitter/TwitterSetting.tsx index 80eeb6d0fa..3e477dcd7d 100644 --- a/apps/web/src/components/Twitter/TwitterSetting.tsx +++ b/apps/web/src/components/Twitter/TwitterSetting.tsx @@ -7,12 +7,13 @@ import { useToastActions } from '~/contexts/toast/ToastContext'; import { TwitterSettingDisconnectMutation } from '~/generated/TwitterSettingDisconnectMutation.graphql'; import { TwitterSettingFragment$key } from '~/generated/TwitterSettingFragment.graphql'; import TwitterIcon from '~/icons/TwitterIcon'; +import { contexts, flows } from '~/shared/analytics/constants'; import { useReportError } from '~/shared/contexts/ErrorReportingContext'; import { usePromisifiedMutation } from '~/shared/relay/usePromisifiedMutation'; import colors from '~/shared/theme/colors'; import { Button } from '../core/Button/Button'; -import InteractiveLink from '../core/InteractiveLink/InteractiveLink'; +import GalleryLink from '../core/GalleryLink/GalleryLink'; import { HStack, VStack } from '../core/Spacer/Stack'; import { BaseM } from '../core/Text/Text'; @@ -94,7 +95,14 @@ export default function TwitterSetting({ queryRef }: Props) { - @@ -110,7 +118,15 @@ export default function TwitterSetting({ queryRef }: Props) { - CONNECT + + CONNECT + @@ -122,7 +138,7 @@ const StyledTwitterSettingContainer = styled(VStack)` background-color: ${colors.faint}; `; -const StyledConnectLink = styled(InteractiveLink)` +const StyledConnectLink = styled(GalleryLink)` text-decoration: none; `; diff --git a/apps/web/src/components/UpsellBanner/UpsellBanner.tsx b/apps/web/src/components/UpsellBanner/UpsellBanner.tsx index 00792b3a60..087d0fbfac 100644 --- a/apps/web/src/components/UpsellBanner/UpsellBanner.tsx +++ b/apps/web/src/components/UpsellBanner/UpsellBanner.tsx @@ -52,6 +52,7 @@ export function UpsellBanner({ queryRef }: Props) { onClose={handleClose} onClick={handleConnectWallet} ctaText="Connect" + experienceFlag="UpsellBanner" /> ); } diff --git a/apps/web/src/components/WalletSelector/GnosisSafePendingMessage.tsx b/apps/web/src/components/WalletSelector/GnosisSafePendingMessage.tsx index 0112590775..a3414cfa39 100644 --- a/apps/web/src/components/WalletSelector/GnosisSafePendingMessage.tsx +++ b/apps/web/src/components/WalletSelector/GnosisSafePendingMessage.tsx @@ -7,6 +7,7 @@ import { Button } from '~/components/core/Button/Button'; import { VStack } from '~/components/core/Spacer/Stack'; import { BaseM } from '~/components/core/Text/Text'; import { EmptyState } from '~/components/EmptyState/EmptyState'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import { LISTENING_ONCHAIN, PendingState, PROMPT_SIGNATURE } from '~/types/Wallet'; import { getLocalStorageItem } from '~/utils/localStorage'; @@ -94,8 +95,22 @@ function GnosisSafePendingMessage({ description="We detected that you previously tried signing a message. Would you like to try authenticating again using the same transaction?" > - - No, sign new message + + + No, sign new message + ) : ( diff --git a/apps/web/src/components/WalletSelector/multichain/DelegateCashMessage.tsx b/apps/web/src/components/WalletSelector/multichain/DelegateCashMessage.tsx index f657c2b254..e12b6cffb5 100644 --- a/apps/web/src/components/WalletSelector/multichain/DelegateCashMessage.tsx +++ b/apps/web/src/components/WalletSelector/multichain/DelegateCashMessage.tsx @@ -1,12 +1,13 @@ import styled from 'styled-components'; import { Button } from '~/components/core/Button/Button'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { VStack } from '~/components/core/Spacer/Stack'; import { BaseM, TitleS } from '~/components/core/Text/Text'; import transitions from '~/components/core/transitions'; import { EmptyState } from '~/components/EmptyState/EmptyState'; import { GALLERY_DISCORD, GALLERY_TWITTER } from '~/constants/urls'; +import { contexts } from '~/shared/analytics/constants'; import { walletIconMap } from './WalletButton'; @@ -25,8 +26,15 @@ export default function DelegateCashMessage({ reset }: Props) { What is it? - Delegate Cash is a - decentralized service that allows you to designate a hot wallet to act and sign on + + Delegate Cash + {' '} + is a decentralized service that allows you to designate a hot wallet to act and sign on behalf of your cold wallet. @@ -34,8 +42,25 @@ export default function DelegateCashMessage({ reset }: Props) { New users Please create a Gallery account with your delegated hot wallet, then reach out to our - team via Discord or{' '} - Twitter for next steps. + team via{' '} + + Discord + {' '} + or{' '} + + Twitter + {' '} + for next steps. @@ -43,11 +68,35 @@ export default function DelegateCashMessage({ reset }: Props) { If you’d like to connect your cold wallet to an existing Gallery account, please reach out to the Gallery team via{' '} - Discord or{' '} - Twitter. + + Discord + {' '} + or{' '} + + Twitter + + . - + ); diff --git a/apps/web/src/components/WalletSelector/multichain/EthereumAddWallet.tsx b/apps/web/src/components/WalletSelector/multichain/EthereumAddWallet.tsx index 74b1ed990a..9f57798ae8 100644 --- a/apps/web/src/components/WalletSelector/multichain/EthereumAddWallet.tsx +++ b/apps/web/src/components/WalletSelector/multichain/EthereumAddWallet.tsx @@ -225,7 +225,14 @@ export const EthereumAddWallet = ({ queryRef, reset, onSuccess = noop }: Props) extension and try again. - attemptAddWallet(account)} disabled={isConnecting}> + attemptAddWallet(account)} + disabled={isConnecting} + > {isConnecting ? 'Connecting...' : 'Confirm'} diff --git a/apps/web/src/components/WalletSelector/multichain/MagicLinkLogin.tsx b/apps/web/src/components/WalletSelector/multichain/MagicLinkLogin.tsx index 0b2c030f2a..68812cdb29 100644 --- a/apps/web/src/components/WalletSelector/multichain/MagicLinkLogin.tsx +++ b/apps/web/src/components/WalletSelector/multichain/MagicLinkLogin.tsx @@ -10,6 +10,7 @@ import { BaseM, TitleS } from '~/components/core/Text/Text'; import { EmptyState } from '~/components/EmptyState/EmptyState'; import { useTrackSignInSuccess } from '~/contexts/analytics/authUtil'; import useMagicLogin from '~/hooks/useMagicLink'; +import { contexts } from '~/shared/analytics/constants'; import { EMAIL_FORMAT } from '~/shared/utils/regex'; import useLoginOrRedirectToOnboarding from '../mutations/useLoginOrRedirectToOnboarding'; @@ -121,10 +122,19 @@ export default function MagicLinkLogin({ reset }: Props) { /> {errorMessage && } - + @@ -236,7 +244,14 @@ export default function GlobalAnnouncementPopover({ queryRef }: Props) { Share your taste with the world. - + ) : ( @@ -254,8 +269,21 @@ export default function GlobalAnnouncementPopover({ queryRef }: Props) { - - + @@ -369,7 +397,14 @@ export default function GlobalAnnouncementPopover({ queryRef }: Props) { Share your taste with the world. - + ) diff --git a/apps/web/src/contexts/globalLayout/GlobalAnnouncementPopover/components/FeaturedCollectorCard.tsx b/apps/web/src/contexts/globalLayout/GlobalAnnouncementPopover/components/FeaturedCollectorCard.tsx index 46c73fd7c7..cef697b9ef 100644 --- a/apps/web/src/contexts/globalLayout/GlobalAnnouncementPopover/components/FeaturedCollectorCard.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalAnnouncementPopover/components/FeaturedCollectorCard.tsx @@ -1,9 +1,9 @@ -import Link from 'next/link'; import { useFragment } from 'react-relay'; import { graphql } from 'relay-runtime'; import styled from 'styled-components'; import breakpoints from '~/components/core/breakpoints'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { TitleM } from '~/components/core/Text/Text'; import transitions from '~/components/core/transitions'; @@ -78,34 +78,30 @@ export default function FeaturedCollectorCard({ } return ( - - - - - {owner.username} - - - - {imageUrls.map((url) => ( - - - - ))} - - - - + + + {owner.username} + + + + {imageUrls.map((url) => ( + + + + ))} + + + ); } -const StyledAnchor = styled.a` - text-decoration: none; -`; - const FeaturedCollectorContainer = styled(VStack)` background: ${colors.offWhite}; border: 1px solid; diff --git a/apps/web/src/contexts/globalLayout/GlobalAnnouncementPopover/useGlobalAnnouncementPopover.tsx b/apps/web/src/contexts/globalLayout/GlobalAnnouncementPopover/useGlobalAnnouncementPopover.tsx index 73bd858232..60cdfc6119 100644 --- a/apps/web/src/contexts/globalLayout/GlobalAnnouncementPopover/useGlobalAnnouncementPopover.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalAnnouncementPopover/useGlobalAnnouncementPopover.tsx @@ -1,12 +1,17 @@ import { useRouter } from 'next/router'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useFragment } from 'react-relay'; import { graphql } from 'relay-runtime'; import { useModalActions } from '~/contexts/modal/ModalContext'; import { useGlobalAnnouncementPopoverFragment$key } from '~/generated/useGlobalAnnouncementPopoverFragment.graphql'; +import { featurePostsPageContentQuery } from '~/pages/features/posts'; +import { PostsFeaturePageContent } from '~/scenes/ContentPages/PostsFeaturePage'; +import useExperience from '~/utils/graphql/experiences/useExperience'; +import { fetchSanityContent } from '~/utils/sanity'; -import GlobalAnnouncementPopover from './GlobalAnnouncementPopover'; +// Keeping for future use +// import GlobalAnnouncementPopover from './GlobalAnnouncementPopover'; type Props = { queryRef: useGlobalAnnouncementPopoverFragment$key; @@ -36,8 +41,8 @@ export default function useGlobalAnnouncementPopover({ } } } - ...GlobalAnnouncementPopoverFragment - # ...useExperienceFragment + # ...GlobalAnnouncementPopoverFragment + ...useExperienceFragment } `, queryRef @@ -48,14 +53,14 @@ export default function useGlobalAnnouncementPopover({ const { asPath, query: urlQuery } = useRouter(); // NOTE: next time we use global announcements, we'll need to set a new flag in the schema - const isGlobalAnnouncementExperienced = true; - // const [isGlobalAnnouncementExperienced, setGlobalAnnouncementExperienced] = useExperience({ - // type: 'YourGlobalAnnouncementFlagHere', - // queryRef: query, - // }) - // const handleDismissGlobalAnnouncement = useCallback(async () => { - // return await setGlobalAnnouncementExperienced() - // }, [setGlobalAnnouncementExperienced]) + // const isGlobalAnnouncementExperienced = true; + const [isGlobalAnnouncementExperienced, setGlobalAnnouncementExperienced] = useExperience({ + type: 'PostsBetaAnnouncement', + queryRef: query, + }); + const handleDismissGlobalAnnouncement = useCallback(async () => { + return await setGlobalAnnouncementExperienced(); + }, [setGlobalAnnouncementExperienced]); // tracks dismissal on session, not persisted across refreshes const [dismissedOnSession, setDismissedOnSession] = useState(false); @@ -94,27 +99,26 @@ export default function useGlobalAnnouncementPopover({ if (dismissVariant === 'session' && dismissedOnSession) return; if (dismissVariant === 'global' && isGlobalAnnouncementExperienced) return; - // TEMPORARY: only display the white rhino launch popover on the homepage - if (asPath !== '/') { - return; - } - if (authRequired && !isAuthenticated) return; if (shouldHidePopoverOnCurrentPath) return; // prevent font flicker on popover load - await handlePreloadFonts(); + // await handlePreloadFonts(); + + const pageContent = await fetchSanityContent(featurePostsPageContentQuery); + + if (!pageContent || !pageContent[0]) return; setTimeout(() => { showModal({ id: 'global-announcement-popover', - content: , + content: , isFullPage: true, headerVariant: 'thicc', }); setDismissedOnSession(true); - // handleDismissGlobalAnnouncement(true); + handleDismissGlobalAnnouncement(); }, popoverDelayMs); } @@ -130,6 +134,7 @@ export default function useGlobalAnnouncementPopover({ dismissVariant, dismissedOnSession, shouldHidePopoverOnCurrentPath, + handleDismissGlobalAnnouncement, ]); useEffect( @@ -144,26 +149,26 @@ export default function useGlobalAnnouncementPopover({ ); } -async function handlePreloadFonts() { - const fontLight = new FontFace( - 'GT Alpina Condensed', - 'url(/fonts/GT-Alpina-Condensed-Light.otf)' - ); - const fontLightItalic = new FontFace( - 'GT Alpina Condensed', - 'url(/fonts/GT-Alpina-Condensed-Light-Italic.otf)' - ); - const fontLight2 = new FontFace( - 'GT Alpina Condensed', - 'url(/fonts/GT-Alpina-Condensed-Light.ttf)' - ); - const fontLightItalic2 = new FontFace( - 'GT Alpina Condensed', - 'url(/fonts/GT-Alpina-Condensed-Light-Italic.ttf)' - ); - - await fontLight.load(); - await fontLightItalic.load(); - await fontLight2.load(); - await fontLightItalic2.load(); -} +// async function handlePreloadFonts() { +// const fontLight = new FontFace( +// 'GT Alpina Condensed', +// 'url(/fonts/GT-Alpina-Condensed-Light.otf)' +// ); +// const fontLightItalic = new FontFace( +// 'GT Alpina Condensed', +// 'url(/fonts/GT-Alpina-Condensed-Light-Italic.otf)' +// ); +// const fontLight2 = new FontFace( +// 'GT Alpina Condensed', +// 'url(/fonts/GT-Alpina-Condensed-Light.ttf)' +// ); +// const fontLightItalic2 = new FontFace( +// 'GT Alpina Condensed', +// 'url(/fonts/GT-Alpina-Condensed-Light-Italic.ttf)' +// ); + +// await fontLight.load(); +// await fontLightItalic.load(); +// await fontLight2.load(); +// await fontLightItalic2.load(); +// } diff --git a/apps/web/src/contexts/globalLayout/GlobalBanner/MobileBetaReleaseBanner.tsx b/apps/web/src/contexts/globalLayout/GlobalBanner/MobileBetaReleaseBanner.tsx index f7843d80eb..872a844903 100644 --- a/apps/web/src/contexts/globalLayout/GlobalBanner/MobileBetaReleaseBanner.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalBanner/MobileBetaReleaseBanner.tsx @@ -4,42 +4,26 @@ import appIcon from 'public/gallery-app-ios-icon.png'; import { useCallback } from 'react'; import styled from 'styled-components'; -import { GlobalBanner } from '~/components/core/GlobalBanner/GlobalBanner'; +import { Button } from '~/components/core/Button/Button'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { TitleXSBold } from '~/components/core/Text/Text'; -import { useTrack } from '~/shared/contexts/AnalyticsContext'; +import { UserExperienceType } from '~/generated/enums'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; type Props = { handleCTAClick: () => void; + experienceFlag: UserExperienceType; }; -export default function MobileBetaReleaseBanner({ handleCTAClick }: Props) { +export default function MobileBetaReleaseBanner({ handleCTAClick, experienceFlag }: Props) { const { push } = useRouter(); - const track = useTrack(); - const handleClick = useCallback(() => { - // TODO: standardize this tracking across all buttons, chips, and icons, like mobile - track('Button Click', { - id: 'Global Banner Button', - name: 'Global Banner Button Clicked', - variant: 'iOS', - }); - push('/mobile'); handleCTAClick(); - }, [handleCTAClick, push, track]); - - return ( - - ); + }, [handleCTAClick, push]); return ( @@ -51,7 +35,13 @@ export default function MobileBetaReleaseBanner({ handleCTAClick }: Props) { Mobile app beta now available - + DOWNLOAD @@ -94,7 +84,7 @@ const StyledImage = styled(Image)` height: 32px; `; -const StyledDownloadButton = styled.button` +const StyledDownloadButton = styled(Button)` background: #3478f6; border-radius: 48px; width: 107px; diff --git a/apps/web/src/contexts/globalLayout/GlobalBanner/MobileBetaUpsell.tsx b/apps/web/src/contexts/globalLayout/GlobalBanner/MobileBetaUpsell.tsx index 97b6bcb6bf..b204e97d22 100644 --- a/apps/web/src/contexts/globalLayout/GlobalBanner/MobileBetaUpsell.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalBanner/MobileBetaUpsell.tsx @@ -6,7 +6,6 @@ import { GlobalBanner } from '~/components/core/GlobalBanner/GlobalBanner'; import { UserExperienceType } from '~/generated/enums'; import { MobileBetaUpsellFragment$key } from '~/generated/MobileBetaUpsellFragment.graphql'; import { useIsMobileWindowWidth } from '~/hooks/useWindowSize'; -import { useTrack } from '~/shared/contexts/AnalyticsContext'; import useExperience from '~/utils/graphql/experiences/useExperience'; import isIOS from '~/utils/isIOS'; @@ -49,8 +48,6 @@ export default function MobileBetaUpsell({ const isAuthenticated = Boolean(query.viewer?.user?.id); const { push } = useRouter(); - const track = useTrack(); - const [isBannerExperienced, setBannerExperienced] = useExperience({ type: experienceFlag, queryRef: query, @@ -61,19 +58,12 @@ export default function MobileBetaUpsell({ }, [setBannerExperienced]); const handleActionClick = useCallback(() => { - // TODO: standardize this tracking across all buttons, chips, and icons, like mobile - track('Button Click', { - id: 'Banner Button', - name: 'Global Banner Button Clicked', - variant: 'default', - }); - push('/mobile'); if (dismissOnActionComponentClick) { hideBanner(); } - }, [dismissOnActionComponentClick, hideBanner, push, track]); + }, [dismissOnActionComponentClick, hideBanner, push]); const isMobile = useIsMobileWindowWidth(); @@ -83,11 +73,14 @@ export default function MobileBetaUpsell({ // TEMPORARY BANNER FOR IOS BETA ANNOUNCEMENT if (isIOS() && isMobile) { - return ; + return ( + + ); } return ( } - + - + FAQ diff --git a/apps/web/src/contexts/globalLayout/GlobalLayoutContext.tsx b/apps/web/src/contexts/globalLayout/GlobalLayoutContext.tsx index ce43d19cbe..81d237ff26 100644 --- a/apps/web/src/contexts/globalLayout/GlobalLayoutContext.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalLayoutContext.tsx @@ -31,8 +31,8 @@ import { PreloadQueryArgs } from '~/types/PageComponentPreloadQuery'; import isTouchscreenDevice from '~/utils/isTouchscreenDevice'; import { FEATURED_COLLECTION_IDS } from './GlobalAnnouncementPopover/GlobalAnnouncementPopover'; +import useGlobalAnnouncementPopover from './GlobalAnnouncementPopover/useGlobalAnnouncementPopover'; import MobileBetaUpsell from './GlobalBanner/MobileBetaUpsell'; -// import useGlobalAnnouncementPopover from './GlobalAnnouncementPopover/useGlobalAnnouncementPopover'; import GlobalSidebar, { GLOBAL_SIDEBAR_DESKTOP_WIDTH } from './GlobalSidebar/GlobalSidebar'; import { FADE_TRANSITION_TIME_MS, @@ -84,7 +84,7 @@ const GlobalLayoutContextQueryNode = graphql` query GlobalLayoutContextQuery { ...GlobalLayoutContextNavbarFragment # Keeping this around for the next time we want to use it - # ...useGlobalAnnouncementPopoverFragment + ...useGlobalAnnouncementPopoverFragment } `; @@ -233,7 +233,7 @@ const GlobalLayoutContextProvider = memo(({ children, preloadedQuery }: Props) = ); // Keeping this around for the next time we want to use it - // useGlobalAnnouncementPopover({ queryRef: query, authRequired: false, dismissVariant: 'global' }); + useGlobalAnnouncementPopover({ queryRef: query, authRequired: false, dismissVariant: 'global' }); const locationKey = useStabilizedRouteTransitionKey(); diff --git a/apps/web/src/contexts/globalLayout/GlobalNavbar/CollectionNavbar/CollectionNavbar.tsx b/apps/web/src/contexts/globalLayout/GlobalNavbar/CollectionNavbar/CollectionNavbar.tsx index d822964e00..e3025d4d69 100644 --- a/apps/web/src/contexts/globalLayout/GlobalNavbar/CollectionNavbar/CollectionNavbar.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalNavbar/CollectionNavbar/CollectionNavbar.tsx @@ -1,10 +1,10 @@ -import Link from 'next/link'; -import { Route, route } from 'nextjs-routes'; +import { Route } from 'nextjs-routes'; import { useMemo } from 'react'; import { useFragment } from 'react-relay'; import { graphql } from 'relay-runtime'; import styled from 'styled-components'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { HStack } from '~/components/core/Spacer/Stack'; import { Paragraph, TITLE_FONT_FAMILY } from '~/components/core/Text/Text'; import NavActionFollow from '~/components/Follow/NavActionFollow'; @@ -16,6 +16,7 @@ import { } from '~/contexts/globalLayout/GlobalNavbar/ProfileDropdown/Breadcrumbs'; import { CollectionNavbarFragment$key } from '~/generated/CollectionNavbarFragment.graphql'; import { useIsMobileOrMobileLargeWindowWidth } from '~/hooks/useWindowSize'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import unescape from '~/shared/utils/unescape'; @@ -93,9 +94,14 @@ export function CollectionNavbar({ queryRef, username, collectionId }: Collectio / - - {galleryName} - + + {galleryName} + / diff --git a/apps/web/src/contexts/globalLayout/GlobalNavbar/CollectionNavbar/CollectionRightContent.tsx b/apps/web/src/contexts/globalLayout/GlobalNavbar/CollectionNavbar/CollectionRightContent.tsx index 9e5a033b71..b906cbe30e 100644 --- a/apps/web/src/contexts/globalLayout/GlobalNavbar/CollectionNavbar/CollectionRightContent.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalNavbar/CollectionNavbar/CollectionRightContent.tsx @@ -17,6 +17,7 @@ import { CollectionRightContentFragment$key } from '~/generated/CollectionRightC import { useIsMobileOrMobileLargeWindowWidth } from '~/hooks/useWindowSize'; import EditUserInfoModal from '~/scenes/UserGalleryPage/EditUserInfoModal'; import LinkButton from '~/scenes/UserGalleryPage/LinkButton'; +import { contexts } from '~/shared/analytics/constants'; import { SignUpButton } from '../SignUpButton'; @@ -118,9 +119,19 @@ export function CollectionRightContent({ {editCollectionUrl && ( - EDIT COLLECTION + )} - NAME & BIO + ); @@ -159,4 +170,5 @@ export function CollectionRightContent({ const EditLinkWrapper = styled.div` position: relative; + cursor: pointer; `; diff --git a/apps/web/src/contexts/globalLayout/GlobalNavbar/EditGalleryNavbar/EditGalleryNavbar.tsx b/apps/web/src/contexts/globalLayout/GlobalNavbar/EditGalleryNavbar/EditGalleryNavbar.tsx index e1af8af023..aaa92beba6 100644 --- a/apps/web/src/contexts/globalLayout/GlobalNavbar/EditGalleryNavbar/EditGalleryNavbar.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalNavbar/EditGalleryNavbar/EditGalleryNavbar.tsx @@ -18,6 +18,7 @@ import { useGuardEditorUnsavedChanges } from '~/hooks/useGuardEditorUnsavedChang import { useSaveHotkey } from '~/hooks/useSaveHotkey'; import { useIsMobileOrMobileLargeWindowWidth } from '~/hooks/useWindowSize'; import { AllGalleriesIcon } from '~/icons/AllGalleriesIcon'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; type Props = { @@ -117,7 +118,16 @@ export function EditGalleryNavbar({ const doneButton = useMemo(() => { if (doneAction === 'no-changes') { - return Done; + return ( + + Done + + ); } else if (doneAction === 'saved') { return ( <> @@ -125,7 +135,13 @@ export function EditGalleryNavbar({ Saved - + Done @@ -137,7 +153,13 @@ export function EditGalleryNavbar({ return ( <> Unsaved changes - + Save diff --git a/apps/web/src/contexts/globalLayout/GlobalNavbar/GalleryNavbar/GalleryLeftContent.tsx b/apps/web/src/contexts/globalLayout/GlobalNavbar/GalleryNavbar/GalleryLeftContent.tsx index 10028ad874..e84726a766 100644 --- a/apps/web/src/contexts/globalLayout/GlobalNavbar/GalleryNavbar/GalleryLeftContent.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalNavbar/GalleryNavbar/GalleryLeftContent.tsx @@ -1,10 +1,10 @@ -import Link from 'next/link'; -import { Route, route } from 'nextjs-routes'; +import { Route } from 'nextjs-routes'; import { useMemo } from 'react'; import { useFragment } from 'react-relay'; import { graphql } from 'relay-runtime'; import styled from 'styled-components'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { HStack } from '~/components/core/Spacer/Stack'; import NavActionFollow from '~/components/Follow/NavActionFollow'; import { @@ -13,6 +13,7 @@ import { } from '~/contexts/globalLayout/GlobalNavbar/ProfileDropdown/Breadcrumbs'; import { GalleryLeftContentFragment$key } from '~/generated/GalleryLeftContentFragment.graphql'; import { useIsMobileOrMobileLargeWindowWidth } from '~/hooks/useWindowSize'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; type Props = { @@ -52,11 +53,14 @@ export default function GalleryLeftContent({ queryRef, galleryName }: Props) { return ( - - - {query.userByUsername?.username} - - + + {query.userByUsername?.username} + / {galleryName || 'Untitled'} ); diff --git a/apps/web/src/contexts/globalLayout/GlobalNavbar/GalleryNavbar/GalleryNavLinks.tsx b/apps/web/src/contexts/globalLayout/GlobalNavbar/GalleryNavbar/GalleryNavLinks.tsx index e99bf656fd..6df3fdde59 100644 --- a/apps/web/src/contexts/globalLayout/GlobalNavbar/GalleryNavbar/GalleryNavLinks.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalNavbar/GalleryNavbar/GalleryNavLinks.tsx @@ -1,11 +1,12 @@ import { useRouter } from 'next/router'; -import { Route, route } from 'nextjs-routes'; +import { Route } from 'nextjs-routes'; import { useMemo } from 'react'; import { graphql, useFragment } from 'react-relay'; import { HStack } from '~/components/core/Spacer/Stack'; import { BaseS } from '~/components/core/Text/Text'; import { GalleryNavLinksFragment$key } from '~/generated/GalleryNavLinksFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; import { removeNullValues } from '~/shared/relay/removeNullValues'; import { NavbarLink } from '../NavbarLink'; @@ -61,9 +62,11 @@ export function GalleryNavLinks({ username, queryRef }: Props) { return ( Featured @@ -71,9 +74,11 @@ export function GalleryNavLinks({ username, queryRef }: Props) { Galleries @@ -82,9 +87,11 @@ export function GalleryNavLinks({ username, queryRef }: Props) { Posts @@ -93,9 +100,11 @@ export function GalleryNavLinks({ username, queryRef }: Props) { Followers diff --git a/apps/web/src/contexts/globalLayout/GlobalNavbar/GalleryNavbar/GalleryRightContent.tsx b/apps/web/src/contexts/globalLayout/GlobalNavbar/GalleryNavbar/GalleryRightContent.tsx index d23bcbcff6..402ee7238e 100644 --- a/apps/web/src/contexts/globalLayout/GlobalNavbar/GalleryNavbar/GalleryRightContent.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalNavbar/GalleryNavbar/GalleryRightContent.tsx @@ -25,6 +25,7 @@ import { useIsMobileOrMobileLargeWindowWidth } from '~/hooks/useWindowSize'; import CogIcon from '~/icons/CogIcon'; import EditUserInfoModal from '~/scenes/UserGalleryPage/EditUserInfoModal'; import LinkButton from '~/scenes/UserGalleryPage/LinkButton'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import { SignUpButton } from '../SignUpButton'; @@ -160,8 +161,20 @@ export function GalleryRightContent({ queryRef, galleryRef, username }: GalleryR return ( - {editGalleryUrl && EDIT GALLERY} - NAME & BIO + {editGalleryUrl && ( + + )} + ); @@ -194,7 +207,13 @@ export function GalleryRightContent({ queryRef, galleryRef, username }: GalleryR if (showShowMultiGalleryButton) { return ( - @@ -216,4 +235,5 @@ export function GalleryRightContent({ queryRef, galleryRef, username }: GalleryR const EditLinkWrapper = styled.div` position: relative; + cursor: pointer; `; diff --git a/apps/web/src/contexts/globalLayout/GlobalNavbar/HomeNavbar/HomeNavbar.tsx b/apps/web/src/contexts/globalLayout/GlobalNavbar/HomeNavbar/HomeNavbar.tsx index f1d394c279..c1d47e23be 100644 --- a/apps/web/src/contexts/globalLayout/GlobalNavbar/HomeNavbar/HomeNavbar.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalNavbar/HomeNavbar/HomeNavbar.tsx @@ -1,11 +1,12 @@ import { useRouter } from 'next/router'; -import { Route, route } from 'nextjs-routes'; +import { Route } from 'nextjs-routes'; import { MouseEventHandler, useCallback } from 'react'; import { graphql, useFragment } from 'react-relay'; import { HStack } from '~/components/core/Spacer/Stack'; import { HomeNavbarFragment$key } from '~/generated/HomeNavbarFragment.graphql'; import { useIsMobileWindowWidth } from '~/hooks/useWindowSize'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import isAdminRole from '~/utils/graphql/isAdminRole'; @@ -71,9 +72,11 @@ export function HomeNavbar({ queryRef }: Props) { {isLoggedIn ? 'For You' : 'Trending'} @@ -81,9 +84,11 @@ export function HomeNavbar({ queryRef }: Props) { {isLoggedIn && ( Following @@ -92,9 +97,11 @@ export function HomeNavbar({ queryRef }: Props) { {(!isLoggedIn || isAdminRole(query)) && ( Latest @@ -102,9 +109,11 @@ export function HomeNavbar({ queryRef }: Props) { Explore diff --git a/apps/web/src/contexts/globalLayout/GlobalNavbar/NavbarLink.tsx b/apps/web/src/contexts/globalLayout/GlobalNavbar/NavbarLink.tsx index f0c565e5ba..efbd0bbeb5 100644 --- a/apps/web/src/contexts/globalLayout/GlobalNavbar/NavbarLink.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalNavbar/NavbarLink.tsx @@ -1,12 +1,14 @@ -import Link from 'next/link'; import styled from 'styled-components'; import breakpoints from '~/components/core/breakpoints'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { BaseS, BODY_FONT_FAMILY } from '~/components/core/Text/Text'; import colors from '~/shared/theme/colors'; // legacyBehavior: false ensures these styles are applied to the link element -export const NavbarLink = styled(Link).attrs({ legacyBehavior: false })<{ active: boolean }>` +export const NavbarLink = styled(GalleryLink).attrs({ legacyBehavior: false })<{ + active: boolean; +}>` font-family: ${BODY_FONT_FAMILY}; line-height: 21px; letter-spacing: -0.04em; diff --git a/apps/web/src/contexts/globalLayout/GlobalNavbar/ProfileDropdown/Breadcrumbs.tsx b/apps/web/src/contexts/globalLayout/GlobalNavbar/ProfileDropdown/Breadcrumbs.tsx index d660bff56e..47c0aca055 100644 --- a/apps/web/src/contexts/globalLayout/GlobalNavbar/ProfileDropdown/Breadcrumbs.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalNavbar/ProfileDropdown/Breadcrumbs.tsx @@ -18,9 +18,10 @@ export const BreadcrumbText = styled(Paragraph)` overflow: hidden; font-size: 18px; + cursor: pointer; `; -export const BreadcrumbLink = styled.a` +export const BreadcrumbLink = styled.span` font-family: ${TITLE_FONT_FAMILY}; font-weight: 400; line-height: 21px; diff --git a/apps/web/src/contexts/globalLayout/GlobalNavbar/SignInButton.tsx b/apps/web/src/contexts/globalLayout/GlobalNavbar/SignInButton.tsx index f7e9f8c10d..a2276d3a2c 100644 --- a/apps/web/src/contexts/globalLayout/GlobalNavbar/SignInButton.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalNavbar/SignInButton.tsx @@ -4,12 +4,21 @@ import breakpoints from '~/components/core/breakpoints'; import TextButton from '~/components/core/Button/TextButton'; import transitions from '~/components/core/transitions'; import useAuthModal from '~/hooks/useAuthModal'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; export function SignInButton() { const showAuthModal = useAuthModal('sign-in'); - return ; + return ( + + ); } const SignInWrapper = styled(TextButton)` diff --git a/apps/web/src/contexts/globalLayout/GlobalNavbar/SignUpButton.tsx b/apps/web/src/contexts/globalLayout/GlobalNavbar/SignUpButton.tsx index cfb2c50197..5377441ac4 100644 --- a/apps/web/src/contexts/globalLayout/GlobalNavbar/SignUpButton.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalNavbar/SignUpButton.tsx @@ -2,11 +2,22 @@ import styled from 'styled-components'; import { Button } from '~/components/core/Button/Button'; import useAuthModal from '~/hooks/useAuthModal'; +import { contexts, flows } from '~/shared/analytics/constants'; export function SignUpButton() { const showAuthModal = useAuthModal('sign-up'); - return Sign up; + return ( + + Sign up + + ); } const StyledButton = styled(Button)` diff --git a/apps/web/src/contexts/globalLayout/GlobalSidebar/SidebarIcon.tsx b/apps/web/src/contexts/globalLayout/GlobalSidebar/SidebarIcon.tsx index 822a7425f5..f3e66aa84c 100644 --- a/apps/web/src/contexts/globalLayout/GlobalSidebar/SidebarIcon.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalSidebar/SidebarIcon.tsx @@ -1,9 +1,9 @@ -import Link from 'next/link'; import { Route } from 'nextjs-routes'; import { MouseEvent, useCallback } from 'react'; import styled from 'styled-components'; import breakpoints from '~/components/core/breakpoints'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import IconContainer from '~/components/core/IconContainer'; import { NewTooltip } from '~/components/Tooltip/NewTooltip'; import { useTooltipHover } from '~/components/Tooltip/useTooltipHover'; @@ -62,7 +62,12 @@ export default function SidebarIcon({ if (href) { return ( - {content} + + {content} + ); } diff --git a/apps/web/src/contexts/globalLayout/GlobalSidebar/SidebarPfp.tsx b/apps/web/src/contexts/globalLayout/GlobalSidebar/SidebarPfp.tsx index d2d72084a0..9e0eef376f 100644 --- a/apps/web/src/contexts/globalLayout/GlobalSidebar/SidebarPfp.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalSidebar/SidebarPfp.tsx @@ -1,5 +1,3 @@ -import Link from 'next/link'; -import { Route } from 'nextjs-routes'; import { graphql, useFragment } from 'react-relay'; import styled from 'styled-components'; @@ -10,11 +8,10 @@ import { SidebarPfpFragment$key } from '~/generated/SidebarPfpFragment.graphql'; type Props = { userRef: SidebarPfpFragment$key; - href: Route; onClick: () => void; }; -export default function SidebarPfp({ userRef, href, onClick }: Props) { +export default function SidebarPfp({ userRef, onClick }: Props) { const user = useFragment( graphql` fragment SidebarPfpFragment on GalleryUser { @@ -27,17 +24,10 @@ export default function SidebarPfp({ userRef, href, onClick }: Props) { useTooltipHover({ placement: 'right' }); return ( - - - - - - + + + + ); } diff --git a/apps/web/src/contexts/globalLayout/GlobalSidebar/StandardSidebar.tsx b/apps/web/src/contexts/globalLayout/GlobalSidebar/StandardSidebar.tsx index ddeb562ab0..076fddcd4d 100644 --- a/apps/web/src/contexts/globalLayout/GlobalSidebar/StandardSidebar.tsx +++ b/apps/web/src/contexts/globalLayout/GlobalSidebar/StandardSidebar.tsx @@ -26,8 +26,8 @@ import { PlusSquareIcon } from '~/icons/PlusSquareIcon'; import SearchIcon from '~/icons/SearchIcon'; import ShopIcon from '~/icons/ShopIcon'; import UserIcon from '~/icons/UserIcon'; -import { useTrack } from '~/shared/contexts/AnalyticsContext'; -import useExperience from '~/utils/graphql/experiences/useExperience'; +import { contexts, flows } from '~/shared/analytics/constants'; +import { GalleryElementTrackingProps, useTrack } from '~/shared/contexts/AnalyticsContext'; import DrawerHeader from './DrawerHeader'; import { useDrawerActions, useDrawerState } from './SidebarDrawerContext'; @@ -61,7 +61,6 @@ export function StandardSidebar({ queryRef }: Props) { } } ...SettingsFragment - ...useExperienceFragment ...useAnnouncementFragment } `, @@ -91,11 +90,6 @@ export function StandardSidebar({ queryRef }: Props) { return 0; }, [query.viewer, totalUnreadAnnouncements]); - const [isMerchStoreUpsellExperienced, setMerchStoreUpsellExperienced] = useExperience({ - type: 'MerchStoreUpsell', - queryRef: query, - }); - const username = (isLoggedIn && query.viewer.user?.username) || ''; const { showModal } = useModalActions(); @@ -133,8 +127,7 @@ export function StandardSidebar({ queryRef }: Props) { const handleShopIconClick = useCallback(async () => { track('Sidebar Shop Click'); - setMerchStoreUpsellExperienced(); - }, [setMerchStoreUpsellExperienced, track]); + }, [track]); const handleHomeIconClick = useCallback(() => { hideDrawer(); @@ -143,47 +136,53 @@ export function StandardSidebar({ queryRef }: Props) { const { captionRef, setCaption } = usePostComposerContext(); - const handleOpenPostComposer = useCallback(() => { - showModal({ - id: 'post-composer', - content: , - headerVariant: 'thicc', - isFullPage: isMobile, - onCloseOverride: (onClose: () => void) => { - if (!captionRef.current) { - onClose(); - return; - } + const handleOpenPostComposer = useCallback( + (eventFlow: GalleryElementTrackingProps['eventFlow']) => { + showModal({ + id: 'post-composer', + content: , + headerVariant: 'thicc', + isFullPage: isMobile, + onCloseOverride: (onClose: () => void) => { + if (!captionRef.current) { + onClose(); + return; + } - showModal({ - headerText: 'Are you sure?', - content: ( - { - onClose(); - }} - onDiscard={() => { - setCaption(''); - onClose(); - }} - /> - ), - isFullPage: false, - }); - }, - }); - }, [captionRef, isMobile, setCaption, showModal]); + showModal({ + headerText: 'Are you sure?', + content: ( + { + onClose(); + }} + onDiscard={() => { + setCaption(''); + onClose(); + }} + /> + ), + isFullPage: false, + }); + }, + }); + }, + [captionRef, isMobile, setCaption, showModal] + ); const handleCreatePostClick = useCallback(() => { + track('Sidebar Create Post Click', { + context: contexts.Posts, + flow: flows['Web Sidebar Post Create Flow'], + }); + hideDrawer(); if (!isLoggedIn) { return; } - handleOpenPostComposer(); - - track('Sidebar Create Post Click'); + handleOpenPostComposer(flows['Web Sidebar Post Create Flow']); }, [hideDrawer, isLoggedIn, handleOpenPostComposer, track]); const handleSearchClick = useCallback(() => { @@ -227,7 +226,7 @@ export function StandardSidebar({ queryRef }: Props) { // feels like a hack but if this hook is run multiple times via parent component re-render, // the same drawer is opened multiple times - const { settings, composer } = routerQuery; + const { settings, composer, referrer } = routerQuery; const isSettingsOpen = useRef(false); const isComposerOpen = useRef(false); @@ -246,13 +245,29 @@ export function StandardSidebar({ queryRef }: Props) { } if (composer === 'true' && !isComposerOpen.current) { + track('Arrive On Gallery', { + context: contexts.Posts, + flow: flows['Share To Gallery'], + authenticated: isLoggedIn, + referrer, + }); if (isLoggedIn) { - handleOpenPostComposer(); + handleOpenPostComposer(flows['Share To Gallery']); return; } showAuthModal(); } - }, [composer, handleOpenPostComposer, isLoggedIn, query, settings, showAuthModal, showDrawer]); + }, [ + composer, + handleOpenPostComposer, + isLoggedIn, + query, + referrer, + settings, + showAuthModal, + showDrawer, + track, + ]); if (isMobile) { return ( @@ -317,11 +332,7 @@ export function StandardSidebar({ queryRef }: Props) { /> {isLoggedIn && query.viewer.user && ( - + } - showUnreadDot={!isMerchStoreUpsellExperienced} /> diff --git a/apps/web/src/contexts/modal/AnimatedModal.tsx b/apps/web/src/contexts/modal/AnimatedModal.tsx index 93dde43ef6..d62b0fea6b 100644 --- a/apps/web/src/contexts/modal/AnimatedModal.tsx +++ b/apps/web/src/contexts/modal/AnimatedModal.tsx @@ -8,6 +8,7 @@ import transitions, { ANIMATED_COMPONENT_TRANSITION_MS, ANIMATED_COMPONENT_TRANSLATION_PIXELS_LARGE, } from '~/components/core/transitions'; +import ErrorBoundary from '~/contexts/boundary/ErrorBoundary'; import { useIsMobileOrMobileLargeWindowWidth } from '~/hooks/useWindowSize'; import { DecoratedCloseIcon } from '~/icons/CloseIcon'; import colors from '~/shared/theme/colors'; @@ -106,7 +107,9 @@ function AnimatedModal({ )} - {content} + + {content} + diff --git a/apps/web/src/contexts/toast/Toast.tsx b/apps/web/src/contexts/toast/Toast.tsx index d720f50012..8edd718ab9 100644 --- a/apps/web/src/contexts/toast/Toast.tsx +++ b/apps/web/src/contexts/toast/Toast.tsx @@ -13,6 +13,7 @@ import transitions, { import AlertIcon from '~/icons/AlertIcon'; import CloseIcon from '~/icons/CloseIcon'; import colors from '~/shared/theme/colors'; +import { noop } from '~/shared/utils/noop'; type Props = { message: string; @@ -21,8 +22,6 @@ type Props = { variant?: 'success' | 'error'; }; -const noop = () => {}; - export function AnimatedToast({ message, onClose = noop, @@ -100,7 +99,11 @@ function Toast({ message, onClose, variant }: Props) { )} - + } /> diff --git a/apps/web/src/contexts/toast/ToastContext.tsx b/apps/web/src/contexts/toast/ToastContext.tsx index babb8794b0..a8008b3bee 100644 --- a/apps/web/src/contexts/toast/ToastContext.tsx +++ b/apps/web/src/contexts/toast/ToastContext.tsx @@ -1,5 +1,7 @@ import { createContext, memo, ReactNode, useCallback, useContext, useMemo, useState } from 'react'; +import { noop } from '~/shared/utils/noop'; + import { AnimatedToast } from './Toast'; type DismissToastHandler = () => void; @@ -29,8 +31,6 @@ export const useToastActions = (): ToastActions => { return context; }; -const noop = () => {}; - type ToastType = { id: string; message: string; diff --git a/apps/web/src/hooks/api/feedEvents/useCommentOnFeedEvent.ts b/apps/web/src/hooks/api/feedEvents/useCommentOnFeedEvent.ts index c9ebe5f253..034f7128cc 100644 --- a/apps/web/src/hooks/api/feedEvents/useCommentOnFeedEvent.ts +++ b/apps/web/src/hooks/api/feedEvents/useCommentOnFeedEvent.ts @@ -112,6 +112,8 @@ export default function useCommentOnFeedEvent() { creationTime: new Date().toISOString(), dbid: optimisticId, id: `Comment:${optimisticId}`, + // TODO: Add mentions to optimistic response when we implement mentions on web + mentions: [], }, }, }, diff --git a/apps/web/src/hooks/api/posts/useCommentOnPost.ts b/apps/web/src/hooks/api/posts/useCommentOnPost.ts index fb992a5740..25e3ec9ed8 100644 --- a/apps/web/src/hooks/api/posts/useCommentOnPost.ts +++ b/apps/web/src/hooks/api/posts/useCommentOnPost.ts @@ -108,6 +108,8 @@ export default function useCommentOnPost() { creationTime: new Date().toISOString(), dbid: optimisticId, id: `Comment:${optimisticId}`, + // TODO: Add mentions to optimistic response when we implement mentions on web + mentions: [], }, }, }, diff --git a/apps/web/src/hooks/api/tokens/useSyncCreatedTokensForExistingContract.ts b/apps/web/src/hooks/api/tokens/useSyncCreatedTokensForExistingContract.ts new file mode 100644 index 0000000000..43be61afdc --- /dev/null +++ b/apps/web/src/hooks/api/tokens/useSyncCreatedTokensForExistingContract.ts @@ -0,0 +1,96 @@ +import { useCallback } from 'react'; +import { graphql } from 'relay-runtime'; + +import { useNftErrorContext } from '~/contexts/NftErrorContext'; +import { useToastActions } from '~/contexts/toast/ToastContext'; +import { useSyncCreatedTokensForExistingContractMutation } from '~/generated/useSyncCreatedTokensForExistingContractMutation.graphql'; +import { removeNullValues } from '~/shared/relay/removeNullValues'; +import { usePromisifiedMutation } from '~/shared/relay/usePromisifiedMutation'; + +export function useSyncCreatedTokensForExistingContract(): [ + (contractId: string) => Promise, + boolean +] { + const [syncCreatedTokensForExistingContractMutate, isContractRefreshing] = + usePromisifiedMutation(graphql` + mutation useSyncCreatedTokensForExistingContractMutation( + $input: SyncCreatedTokensForExistingContractInput! + ) { + syncCreatedTokensForExistingContract(input: $input) { + ... on SyncCreatedTokensForExistingContractPayload { + __typename + viewer { + # This should be sufficient to capture all the things + # we want to refresh. Don't @me when this fails. + ...GalleryEditorViewerFragment + # Refresh tokens for post composer + ...NftSelectorViewerFragment + + ... on Viewer { + user { + tokens(ownershipFilter: [Creator, Holder]) { + dbid + } + } + } + } + } + ... on ErrNotAuthorized { + __typename + message + } + ... on ErrSyncFailed { + __typename + message + } + } + } + `); + + const { clearTokenFailureState } = useNftErrorContext(); + const { pushToast } = useToastActions(); + + const syncCreatedTokensForExistingContract = useCallback( + async (contractId: string) => { + pushToast({ + message: 'We’re retrieving your new pieces. This may take up to a few minutes.', + autoClose: true, + }); + + function showFailure() { + pushToast({ + autoClose: true, + message: + "Something went wrong while syncing your tokens. We're looking into it. Please try again in a few minutes.", + }); + } + + try { + const response = await syncCreatedTokensForExistingContractMutate({ + variables: { input: { contractId } }, + }); + + if ( + response.syncCreatedTokensForExistingContract?.__typename !== + 'SyncCreatedTokensForExistingContractPayload' + ) { + showFailure(); + } else { + const tokenIds = removeNullValues( + response.syncCreatedTokensForExistingContract.viewer?.user?.tokens?.map((token) => { + return token?.dbid; + }) + ); + + clearTokenFailureState(tokenIds); + } + } catch (error) { + showFailure(); + } + }, + + [syncCreatedTokensForExistingContractMutate, clearTokenFailureState, pushToast] + ); + + return [syncCreatedTokensForExistingContract, isContractRefreshing]; +} diff --git a/apps/web/src/hooks/useMintContract.tsx b/apps/web/src/hooks/useMintContract.tsx index ca397de5b3..215e5dc97c 100644 --- a/apps/web/src/hooks/useMintContract.tsx +++ b/apps/web/src/hooks/useMintContract.tsx @@ -23,6 +23,7 @@ export default function useMintContract({ contract, tokenId, allowlist, onMintSu const [error, setError] = useState(''); const [transactionStatus, setTransactionStatus] = useState(null); const [transactionHash, setTransactionHash] = useState<`0x${string}` | undefined>(); + const [quantity, setQuantity] = useState(1); const address = rawAddress?.toLowerCase(); @@ -32,9 +33,15 @@ export default function useMintContract({ contract, tokenId, allowlist, onMintSu if (contract && address) { try { const merkleProof = allowlist ? generateMerkleProof(address, Array.from(allowlist)) : []; - console.log({ contract: contract.address, tokenId, address, merkleProof }); - hash = await contract.write.mint([tokenId, address, merkleProof], { - value: ethers.utils.parseEther('0.000777'), + console.log({ + contract: contract.address, + tokenId, + address, + amount: quantity, + merkleProof, + }); + hash = await contract.write.mint([tokenId, address, quantity, merkleProof], { + value: ethers.utils.parseEther(`${quantity * 0.000777}`), }); if (hash) { @@ -86,7 +93,7 @@ export default function useMintContract({ contract, tokenId, allowlist, onMintSu setTransactionStatus(TransactionStatus.FAILED); setError('The transaction was unsuccesful. Please check Basescan for details.'); } - }, [address, allowlist, contract, onMintSuccess, tokenId]); + }, [address, allowlist, contract, onMintSuccess, quantity, tokenId]); const handleNetworkSwitchError = useCallback(() => { setTransactionStatus(TransactionStatus.FAILED); @@ -167,5 +174,7 @@ export default function useMintContract({ contract, tokenId, allowlist, onMintSu error, handleClick, buttonText, + quantity, + setQuantity, }; } diff --git a/apps/web/src/icons/AdmireIcon.tsx b/apps/web/src/icons/AdmireIcon.tsx deleted file mode 100644 index f9c26d8bcd..0000000000 --- a/apps/web/src/icons/AdmireIcon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -export function AdmireIcon() { - return ( - - - - - - - - - - - - - ); -} diff --git a/apps/web/src/scenes/BasicTextPage/BasicTextPage.tsx b/apps/web/src/scenes/BasicTextPage/BasicTextPage.tsx index 0b0abc877a..ac5e736d9a 100644 --- a/apps/web/src/scenes/BasicTextPage/BasicTextPage.tsx +++ b/apps/web/src/scenes/BasicTextPage/BasicTextPage.tsx @@ -16,7 +16,7 @@ export default function BasicTextPage({ title, body }: Props) { {title} - + diff --git a/apps/web/src/scenes/CollectionGalleryPage/CollectionGalleryHeader.tsx b/apps/web/src/scenes/CollectionGalleryPage/CollectionGalleryHeader.tsx index ad57efc36b..5a0b910f98 100644 --- a/apps/web/src/scenes/CollectionGalleryPage/CollectionGalleryHeader.tsx +++ b/apps/web/src/scenes/CollectionGalleryPage/CollectionGalleryHeader.tsx @@ -1,4 +1,3 @@ -import { route } from 'nextjs-routes'; import { useCallback, useMemo } from 'react'; import { graphql, useFragment } from 'react-relay'; import styled from 'styled-components'; @@ -21,7 +20,7 @@ import { CollectionGalleryHeaderQueryFragment$key } from '~/generated/Collection import useUpdateCollectionInfo from '~/hooks/api/collections/useUpdateCollectionInfo'; import { useIsMobileOrMobileLargeWindowWidth } from '~/hooks/useWindowSize'; import MobileLayoutToggle from '~/scenes/UserGalleryPage/MobileLayoutToggle'; -import { useTrack } from '~/shared/contexts/AnalyticsContext'; +import { contexts } from '~/shared/analytics/constants'; import unescape from '~/shared/utils/unescape'; import GalleryTitleBreadcrumb from '../UserGalleryPage/GalleryTitleBreadcrumb'; @@ -90,22 +89,11 @@ function CollectionGalleryHeader({ [collection.collectorsNote] ); - const track = useTrack(); - const { dbid: collectionId, gallery: { dbid: galleryId }, } = collection; - const handleShareClick = useCallback(() => { - track('Share Collection', { - path: route({ - pathname: '/[username]/[collectionId]', - query: { username: username as string, collectionId }, - }), - }); - }, [collectionId, username, track]); - const showEditActions = username?.toLowerCase() === query.viewer?.user?.username?.toLowerCase(); const collectionUrl = window.location.href; @@ -150,7 +138,7 @@ function CollectionGalleryHeader({ {unescapedCollectorsNote ? ( - + ) : (
@@ -177,7 +165,7 @@ function CollectionGalleryHeader({ {unescapedCollectorsNote && ( - + )} @@ -186,15 +174,24 @@ function CollectionGalleryHeader({ {showEditActions ? ( <> - + {/* On mobile, we show these options in the navbar, not in header */} - - Edit Name & Description - + {!shouldDisplayMobileLayoutToggle && ( { - track('Update existing collection'); - }} - > - Edit Collection - + name="Manage Collection" + eventContext={contexts.UserCollection} + label="Edit Collection" + /> )} ) : ( - + )} diff --git a/apps/web/src/scenes/CommunityPage/CommunityPageDisabled.tsx b/apps/web/src/scenes/CommunityPage/CommunityPageDisabled.tsx deleted file mode 100644 index 92d3fe1362..0000000000 --- a/apps/web/src/scenes/CommunityPage/CommunityPageDisabled.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import styled from 'styled-components'; - -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; -import { VStack } from '~/components/core/Spacer/Stack'; -import { BaseM, TitleDiatypeM } from '~/components/core/Text/Text'; -import { GALLERY_DISCORD, GALLERY_TWITTER } from '~/constants/urls'; - -type Props = { - name: string; -}; - -export default function CommunityPageDisabled({ name }: Props) { - return ( - - Coming Soon - - - We are working to enable community pages for individual projects under the {name} contract. - - - Be the first to know when it's available by joining us on{' '} - Discord or{' '} - Twitter. - - - Back to home - - - ); -} - -const StyledDisabledSection = styled(VStack)` - margin-top: 20vh; // position a bit higher than the center - - text-align: center; -`; diff --git a/apps/web/src/scenes/CommunityPage/CommunityPageMetadata.tsx b/apps/web/src/scenes/CommunityPage/CommunityPageMetadata.tsx index e7b5f697a5..9fd19bb4b8 100644 --- a/apps/web/src/scenes/CommunityPage/CommunityPageMetadata.tsx +++ b/apps/web/src/scenes/CommunityPage/CommunityPageMetadata.tsx @@ -1,4 +1,3 @@ -import Link from 'next/link'; import { Route } from 'nextjs-routes'; import { useCallback, useMemo } from 'react'; import { graphql, useFragment } from 'react-relay'; @@ -6,7 +5,7 @@ import styled from 'styled-components'; import breakpoints from '~/components/core/breakpoints'; import { Button } from '~/components/core/Button/Button'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { BaseM, TitleXS } from '~/components/core/Text/Text'; import { PostComposerModalWithSelector } from '~/components/Posts/PostComposerModal'; @@ -18,7 +17,7 @@ import { CommunityPageMetadataFragment$key } from '~/generated/CommunityPageMeta import { CommunityPageMetadataQueryFragment$key } from '~/generated/CommunityPageMetadataQueryFragment.graphql'; import { useIsMobileWindowWidth } from '~/hooks/useWindowSize'; import { PlusSquareIcon } from '~/icons/PlusSquareIcon'; -import { useTrack } from '~/shared/contexts/AnalyticsContext'; +import { contexts, flows } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import { chains } from '~/shared/utils/chains'; import { getExternalAddressLink, truncateAddress } from '~/shared/utils/wallet'; @@ -35,6 +34,9 @@ export default function CommunityPageMetadata({ communityRef, queryRef }: Props) graphql` fragment CommunityPageMetadataFragment on Community { name + contract { + dbid + } contractAddress { chain address @@ -94,12 +96,10 @@ export default function CommunityPageMetadata({ communityRef, queryRef }: Props) const { isMemberOfCommunity, refetchIsMemberOfCommunity } = useIsMemberOfCommunity(); - const track = useTrack(); const { showModal } = useModalActions(); const isMobile = useIsMobileWindowWidth(); const handleCreatePostClick = useCallback(() => { - track('Community Page: Clicked Enabled Post Button'); if (query?.viewer?.__typename !== 'Viewer') { return; } @@ -108,18 +108,26 @@ export default function CommunityPageMetadata({ communityRef, queryRef }: Props) content: ( ), headerVariant: 'thicc', isFullPage: isMobile, }); - }, [track, showModal, query, community.name, community.contractAddress?.address, isMobile]); + }, [ + showModal, + query, + community.name, + community.contractAddress?.address, + community.contract?.dbid, + isMobile, + ]); const handleDisabledPostButtonClick = useCallback(() => { - track('Community Page: Clicked Disabled Post Button'); showModal({ content: ( - + {creatorUsername} - + ); } if (creatorExternalLink) { return ( - + {truncateAddress(creatorAddress || '')} - + ); } return null; @@ -175,14 +193,24 @@ export default function CommunityPageMetadata({ communityRef, queryRef }: Props) {showPostButton && (isMemberOfCommunity ? ( - + Post ) : ( - + Post @@ -220,10 +248,6 @@ const StyledBaseM = styled(BaseM)` color: inherit; `; -const StyledLink = styled(Link)` - text-decoration: none; -`; - const StyledPostButton = styled(Button)` width: 100px; height: 32px; diff --git a/apps/web/src/scenes/CommunityPage/CommunityPageOwnershipRequiredModal.tsx b/apps/web/src/scenes/CommunityPage/CommunityPageOwnershipRequiredModal.tsx index 843ed6e33e..55784fe382 100644 --- a/apps/web/src/scenes/CommunityPage/CommunityPageOwnershipRequiredModal.tsx +++ b/apps/web/src/scenes/CommunityPage/CommunityPageOwnershipRequiredModal.tsx @@ -9,6 +9,7 @@ import { useModalActions } from '~/contexts/modal/ModalContext'; import { CommunityPageOwnershipRequiredModalFragment$key } from '~/generated/CommunityPageOwnershipRequiredModalFragment.graphql'; import useSyncTokens from '~/hooks/api/tokens/useSyncTokens'; import { RefreshIcon } from '~/icons/RefreshIcon'; +import { contexts } from '~/shared/analytics/constants'; type Props = { communityRef: CommunityPageOwnershipRequiredModalFragment$key; @@ -53,13 +54,26 @@ export default function CommunityPageOwnershipRequiredModal({ not displaying try refreshing your collection. - - diff --git a/apps/web/src/scenes/CommunityPage/CommunityPagePostsTab.tsx b/apps/web/src/scenes/CommunityPage/CommunityPagePostsTab.tsx index 820f86c62e..df8c449d25 100644 --- a/apps/web/src/scenes/CommunityPage/CommunityPagePostsTab.tsx +++ b/apps/web/src/scenes/CommunityPage/CommunityPagePostsTab.tsx @@ -14,6 +14,7 @@ import { CommunityPagePostsTabFragment$key } from '~/generated/CommunityPagePost import { CommunityPagePostsTabQueryFragment$key } from '~/generated/CommunityPagePostsTabQueryFragment.graphql'; import { RefetchableCommunityFeedQuery } from '~/generated/RefetchableCommunityFeedQuery.graphql'; import { useIsMobileOrMobileLargeWindowWidth } from '~/hooks/useWindowSize'; +import { contexts, flows } from '~/shared/analytics/constants'; type Props = { communityRef: CommunityPagePostsTabFragment$key; @@ -41,6 +42,9 @@ export default function CommunityPagePostsTab({ communityRef, queryRef }: Props) } } name + contract { + dbid + } contractAddress { address } @@ -93,15 +97,24 @@ export default function CommunityPagePostsTab({ communityRef, queryRef }: Props) content: ( ), headerVariant: 'thicc', isFullPage: isMobile, }); - }, [showModal, query, community.name, community.contractAddress?.address, isMobile]); + }, [ + showModal, + query, + community.name, + community.contractAddress?.address, + community.contract?.dbid, + isMobile, + ]); const { isMemberOfCommunity } = useIsMemberOfCommunity(); @@ -115,7 +128,14 @@ export default function CommunityPagePostsTab({ communityRef, queryRef }: Props) {community.name ? {community.name} : 'this community'} and inspire others! - Create a Post + + Create a Post + ) : ( diff --git a/apps/web/src/scenes/CommunityPage/CommunityPageViewHeader.tsx b/apps/web/src/scenes/CommunityPage/CommunityPageViewHeader.tsx index f24e31e4a3..8c0bf13fb6 100644 --- a/apps/web/src/scenes/CommunityPage/CommunityPageViewHeader.tsx +++ b/apps/web/src/scenes/CommunityPage/CommunityPageViewHeader.tsx @@ -6,8 +6,8 @@ import styled from 'styled-components'; import CopyToClipboard from '~/components/CopyToClipboard/CopyToClipboard'; import breakpoints from '~/components/core/breakpoints'; import TextButton from '~/components/core/Button/TextButton'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import IconContainer from '~/components/core/IconContainer'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; import Markdown from '~/components/core/Markdown/Markdown'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { BaseM, TitleDiatypeL, TitleL } from '~/components/core/Text/Text'; @@ -17,6 +17,7 @@ import { CommunityPageViewHeaderQueryFragment$key } from '~/generated/CommunityP import { useIsMobileWindowWidth } from '~/hooks/useWindowSize'; import GlobeIcon from '~/icons/GlobeIcon'; import ShareIcon from '~/icons/ShareIcon'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import { replaceUrlsWithMarkdownFormat } from '~/shared/utils/replaceUrlsWithMarkdownFormat'; import { getExternalAddressLink, truncateAddress } from '~/shared/utils/wallet'; @@ -107,14 +108,19 @@ export default function CommunityPageViewHeader({ communityRef, queryRef }: Prop return ( {externalAddressLink && ( - + } onClick={handleExternalLinkClick} /> - + )} - + {isLineClampEnabled && ( )} diff --git a/apps/web/src/scenes/ContentPages/ChangelogPage.tsx b/apps/web/src/scenes/ContentPages/ChangelogPage.tsx index 047e5eea31..6148e10e36 100644 --- a/apps/web/src/scenes/ContentPages/ChangelogPage.tsx +++ b/apps/web/src/scenes/ContentPages/ChangelogPage.tsx @@ -7,6 +7,7 @@ import breakpoints, { pageGutter } from '~/components/core/breakpoints'; import Markdown from '~/components/core/Markdown/Markdown'; import { VStack } from '~/components/core/Spacer/Stack'; import { BaseM, TitleDiatypeL } from '~/components/core/Text/Text'; +import { contexts } from '~/shared/analytics/constants'; export type ChangelogSection = { header: string; @@ -46,13 +47,16 @@ export default function ChangelogPage({ sections }: Props) { {section.summary && ( - + )} Changes and Improvements - + diff --git a/apps/web/src/scenes/ContentPages/ContentModules/Faq.tsx b/apps/web/src/scenes/ContentPages/ContentModules/Faq.tsx new file mode 100644 index 0000000000..17ff6010f6 --- /dev/null +++ b/apps/web/src/scenes/ContentPages/ContentModules/Faq.tsx @@ -0,0 +1,177 @@ +import { useEffect, useRef, useState } from 'react'; +import { animated, useSpring } from 'react-spring'; +import styled from 'styled-components'; + +import breakpoints from '~/components/core/breakpoints'; +import { HStack, VStack } from '~/components/core/Spacer/Stack'; +import { TitleDiatypeL } from '~/components/core/Text/Text'; +import colors from '~/shared/theme/colors'; + +import { CmsTypes } from '../cms_types'; + +type FaqPairProps = { + content: CmsTypes.Faq; + isOpen: boolean; + onClick: () => void; +}; + +// A pair of question + answer for the FAQ section +function FaqPair({ content, isOpen, onClick }: FaqPairProps) { + const contentRef = useRef(null); + const [contentHeight, setContentHeight] = useState(0); + + useEffect(() => { + if (contentRef.current) { + setContentHeight(contentRef.current.scrollHeight); + } + }, [content]); + + const toggleExpansion = useSpring({ + opacity: isOpen ? 1 : 0, + height: isOpen ? `${contentHeight}px` : '0px', + config: { duration: 200, easing: (t) => t }, + }); + + const rotatePlusIcon = useSpring({ + transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)', + config: { duration: 200, easing: (t) => t }, + }); + + const togglePlusIconOpacity = useSpring({ + opacity: isOpen ? 0 : 1, + config: { duration: 200, easing: (t) => t }, + }); + + return ( + + +
+ {content.question} +
+ + {content.answer} + +
+ +
+ ); +} + +const AnimatedAnswerContainer = styled(animated.div)` + overflow: hidden; + box-sizing: border-box; +`; + +const Line = styled(animated.div)` + position: absolute; + background-color: black; + left: 50%; + transform: translateX(-50%); +`; + +const HorizontalLine = styled(Line)` + width: 100%; + height: 1px; + top: 50%; +`; + +const VerticalLine = styled(Line)` + width: 1px; + height: 100%; + top: 0; +`; + +const Button = styled(animated.div)` + margin-top: 8px; + position: relative; + width: 12px; + min-width: 12px; + height: 12px; + cursor: pointer; +`; + +const StyledFaqPair = styled(HStack)<{ isOpen: boolean }>` + background-color: ${({ isOpen }) => (isOpen ? colors.offWhite : 'none')}; + cursor: pointer; + transition: background-color 0.3s ease-in-out; + padding: 8px 16px; +`; + +const StyledQuestionText = styled(TitleDiatypeL)` + font-weight: 400; + + @media only screen and ${breakpoints.desktop} { + font-size: 20px; + line-height: 32px; + } +`; + +const StyledAnswerText = styled(TitleDiatypeL)` + padding: 8px 0; + font-size: 16px; + line-height: 24px; + font-weight: 400; +`; + +type Props = { + content: CmsTypes.FaqModule; +}; + +// The FAQ section which contains a list of FaqPairs. +export default function Faq({ content }: Props) { + const [openIndex, setOpenIndex] = useState(null); + + const handleClick = (index: number) => { + setOpenIndex(openIndex === index ? null : index); + }; + + return ( + + + Frequently asked questions + + {content.faqs.map((faq, index) => ( + handleClick(index)} + /> + ))} + + + + ); +} + +const StyledSection = styled(HStack)` + background-color: ${colors.faint}; + width: 100%; + padding: 48px 32px; + margin-bottom: 96px; + + @media only screen and ${breakpoints.desktop} { + padding: 72px 0; + } +`; + +const StyledContainer = styled(VStack)` + @media only screen and ${breakpoints.desktop} { + width: 943px; + flex-direction: row; + } +`; + +const StyledTitle = styled(TitleDiatypeL)` + font-weight: 400; + font-size: 24px; + line-height: 28px; + + @media only screen and ${breakpoints.desktop} { + font-size: 36px; + line-height: 42px; + } +`; diff --git a/apps/web/src/scenes/ContentPages/ContentModules/FeatureHighlight.tsx b/apps/web/src/scenes/ContentPages/ContentModules/FeatureHighlight.tsx new file mode 100644 index 0000000000..85f12bf8cd --- /dev/null +++ b/apps/web/src/scenes/ContentPages/ContentModules/FeatureHighlight.tsx @@ -0,0 +1,131 @@ +import { useMemo } from 'react'; +import styled from 'styled-components'; + +import breakpoints from '~/components/core/breakpoints'; +import { VStack } from '~/components/core/Spacer/Stack'; +import { TitleCondensed, TitleDiatypeL } from '~/components/core/Text/Text'; +import colors from '~/shared/theme/colors'; + +import { CmsTypes } from '../cms_types'; + +type FeatureHighlightBulletsProps = { + bullets: CmsTypes.FeatureHighlight['body']; +}; + +function FeatureHighlightBullets({ bullets }: FeatureHighlightBulletsProps) { + const textList = useMemo( + () => bullets.map((bullet) => bullet.children[0] && bullet.children[0]?.text), + [bullets] + ); + return ( + + + {textList.map((text, index) => ( +
  • + {text} +
  • + ))} +
    +
    + ); +} + +const StyledList = styled.ul` + padding-left: 24px; +`; + +type FeatureHighlightMediaProps = { + media: CmsTypes.FeatureHighlight['media']; +}; + +function FeatureHighlightMedia({ media }: FeatureHighlightMediaProps) { + if (media.video) { + return ; + } + + if (media.image) { + return ; + } + + return null; +} + +type Props = { + content: CmsTypes.FeatureHighlight; +}; + +export default function FeatureHighlight({ content }: Props) { + return ( + + + {content.heading} + + + {content.media && ( + + + + )} + + ); +} + +const StyledHighlight = styled(VStack)<{ orientation: string }>` + align-items: center; + margin: 0 32px; + + @media only screen and ${breakpoints.desktop} { + flex-direction: ${({ orientation }) => (orientation === 'right' ? 'row-reverse' : 'row')}; + gap: 0 32px; + margin: 0; + } +`; + +const StyledTitle = styled(TitleCondensed)` + font-size: 48px; + text-align: start; + @media only screen and ${breakpoints.desktop} { + font-size: 64px; + } +`; + +const StyledTextSection = styled(VStack)` + align-items: flex-start; + + @media only screen and ${breakpoints.tablet} { + max-width: 480px; + } +`; + +const StyledMedia = styled.div` + min-width: 326px; + min-height: 326px; + max-width: 326px; + max-height: 326px; + background-color: ${colors.faint}; + + @media only screen and ${breakpoints.desktop} { + min-width: 500px; + min-height: 500px; + max-width: 500px; + max-height: 500px; + } +`; +const StyledVideo = styled.video` + width: 100%; + height: 100%; +`; +const StyledImage = styled.img` + width: 100%; + height: 100%; +`; + +const StyledText = styled(TitleDiatypeL)` + font-weight: 400; + font-size: 16px; + line-height: 20px; + @media only screen and ${breakpoints.desktop} { + font-size: 20px; + line-height: 28px; + } +`; diff --git a/apps/web/src/scenes/ContentPages/MobileAppLandingPage.tsx b/apps/web/src/scenes/ContentPages/MobileAppLandingPage.tsx index 99fd26f4f1..f4faa0269c 100644 --- a/apps/web/src/scenes/ContentPages/MobileAppLandingPage.tsx +++ b/apps/web/src/scenes/ContentPages/MobileAppLandingPage.tsx @@ -3,9 +3,11 @@ import styled from 'styled-components'; import breakpoints from '~/components/core/breakpoints'; import { Button } from '~/components/core/Button/Button'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { VStack } from '~/components/core/Spacer/Stack'; import { BaseXL, TitleDiatypeL, TitleL } from '~/components/core/Text/Text'; import { useGlobalNavbarHeight } from '~/contexts/globalLayout/GlobalNavbar/useGlobalNavbarHeight'; +import { contexts } from '~/shared/analytics/constants'; export default function MobileAppLandingPage() { const navbarHeight = useGlobalNavbarHeight(); @@ -36,7 +38,13 @@ export default function MobileAppLandingPage() { target="_blank" rel="noreferrer" > - Download The App + + Download The App + @@ -71,7 +79,7 @@ const GiantTitle = styled(TitleL)` } `; -const StyledLink = styled.a` +const StyledLink = styled(GalleryLink)` text-decoration: none; `; diff --git a/apps/web/src/scenes/ContentPages/PostsFeaturePage.tsx b/apps/web/src/scenes/ContentPages/PostsFeaturePage.tsx new file mode 100644 index 0000000000..18715e58c7 --- /dev/null +++ b/apps/web/src/scenes/ContentPages/PostsFeaturePage.tsx @@ -0,0 +1,206 @@ +import Head from 'next/head'; +import { Route } from 'nextjs-routes'; +import { useMemo } from 'react'; +import { graphql, useLazyLoadQuery } from 'react-relay'; +import styled from 'styled-components'; + +import breakpoints, { pageGutter } from '~/components/core/breakpoints'; +import { Button } from '~/components/core/Button/Button'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; +import Markdown from '~/components/core/Markdown/Markdown'; +import { VStack } from '~/components/core/Spacer/Stack'; +import { TitleCondensed, TitleDiatypeL, TitleXS } from '~/components/core/Text/Text'; +import { PostsFeaturePageContentQuery } from '~/generated/PostsFeaturePageContentQuery.graphql'; +import { contexts, flows } from '~/shared/analytics/constants'; +import colors from '~/shared/theme/colors'; + +import { CmsTypes } from './cms_types'; +import Faq from './ContentModules/Faq'; +import FeatureHighlight from './ContentModules/FeatureHighlight'; + +type Props = { + pageContent: CmsTypes.FeaturePage; +}; + +export default function PostsFeaturePage({ pageContent }: Props) { + return ( + <> + + Gallery | Posts + + + + ); +} + +export function PostsFeaturePageContent({ pageContent }: Props) { + const query = useLazyLoadQuery( + graphql` + query PostsFeaturePageContentQuery { + viewer { + __typename + } + } + `, + {} + ); + + const isAuthenticated = query.viewer?.__typename === 'Viewer'; + const ctaButtonRoute: Route = useMemo(() => { + if (!isAuthenticated) return { pathname: '/auth' }; + + return { pathname: '/home', query: { composer: 'true' } }; + }, [isAuthenticated]); + + return ( + + + + + + + Introducing + + Posts + + + + BETA + + + {pageContent.introText} + + + + Get Started + + + + + + {pageContent.featureHighlights?.map((highlight) => ( + + ))} + + + {pageContent.externalLink && ( + + + + )} + + + Get Started + + + + + + + ); +} + +const StyledPage = styled(VStack)` + align-items: flex-start; + min-height: 100vh; + margin-top: 32px; + + @media only screen and ${breakpoints.desktop} { + align-items: center; + } +`; + +const StyledContent = styled(VStack)` + width: 100%; + @media only screen and ${breakpoints.desktop} { + width: 1080px; + margin: 80px ${pageGutter.mobile}px 0; + } +`; + +const StyledIntro = styled(VStack)` + margin: 0 32px; +`; + +const StyledHeading = styled(TitleCondensed)` + font-size: 72px; + line-height: 56px; + width: 100%; + + font-weight: 400; + + strong { + font-style: italic; + font-weight: 400; + } + + @media only screen and ${breakpoints.desktop} { + font-size: 128px; + line-height: 96px; + width: 500px; + } +`; + +const StyledBetaPill = styled.div` + width: fit-content; + border-radius: 24px; + padding: 4px 12px; + border: 1px solid ${colors.activeBlue}; +`; + +const StyledSubheading = styled(TitleDiatypeL)` + font-size: 16px; + line-height: 20px; + font-weight: 400; + text-align: center; + + a { + font-size: 16px; + } + + @media only screen and ${breakpoints.tablet} { + max-width: 326px; + } + + @media only screen and ${breakpoints.desktop} { + font-size: 24px; + line-height: 32px; + max-width: 500px; + a { + font-size: 24px; + } + } +`; + +const GetStartedButton = styled(Button)` + text-transform: initial; + padding: 8px 32px; + width: fit-content; + + &:hover { + opacity: 0.8; + } +`; + +const StyledSplashImage = styled.img` + width: 100%; + + @media only screen and ${breakpoints.tablet} { + max-width: 480px; + } + @media only screen and ${breakpoints.desktop} { + max-width: 720px; + } +`; diff --git a/apps/web/src/scenes/ContentPages/cms_types.ts b/apps/web/src/scenes/ContentPages/cms_types.ts new file mode 100644 index 0000000000..d324dcaceb --- /dev/null +++ b/apps/web/src/scenes/ContentPages/cms_types.ts @@ -0,0 +1,64 @@ +// Types that represent the content models in our CMS. These should be manually updated whenever we update the CMS model schema. + +interface Block { + _type: 'block'; + style: string; + list: string; + children: { _key: string; _type: 'span'; text: string; marks: string[] }[]; +} + +interface Image { + _type: 'image'; + asset: { + url: string; + }; + alt: string; +} + +interface Media { + _type: 'media'; + mediaType: 'image' | 'video'; + image: Image; + video: Video; + alt: string; +} + +interface Video { + _type: 'video'; + asset: { + url: string; + }; +} + +export namespace CmsTypes { + export interface Faq { + _type: 'faq'; + question: string; + answer: string; + } + + export interface FaqModule { + _type: 'faqModule'; + title: string; + faqs: Faq[]; + } + + export interface FeatureHighlight { + _type: 'featureHighlight'; + heading: string; + body: Block[]; + media: Media; + orientation: 'left' | 'right'; + } + + export interface FeaturePage { + _type: 'featurePage'; + id: string; + title: string; + introText: string; + splashImage: Image; + featureHighlights: FeatureHighlight[]; + faqModule: FaqModule; + externalLink: string; + } +} diff --git a/apps/web/src/scenes/LandingPage/LandingPage.tsx b/apps/web/src/scenes/LandingPage/LandingPage.tsx index 2e8b8b73a9..a7ed2d14c0 100644 --- a/apps/web/src/scenes/LandingPage/LandingPage.tsx +++ b/apps/web/src/scenes/LandingPage/LandingPage.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { ButtonLink } from '~/components/core/Button/Button'; +import { DeprecatedButtonLink } from '~/components/core/Button/Button'; import NavLink from '~/components/core/NavLink/NavLink'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { BaseS, BlueLabel, TitleM } from '~/components/core/Text/Text'; @@ -20,20 +20,20 @@ export default function LandingPage() { - track('Landing page Sign In button click')} data-testid="sign-in-button" > Sign In - - + track('Landing page Explore button click')} variant="secondary" > Explore - + diff --git a/apps/web/src/scenes/MaintenancePage/MaintenancePage.tsx b/apps/web/src/scenes/MaintenancePage/MaintenancePage.tsx index 1d22aa15bf..d824e65386 100644 --- a/apps/web/src/scenes/MaintenancePage/MaintenancePage.tsx +++ b/apps/web/src/scenes/MaintenancePage/MaintenancePage.tsx @@ -2,7 +2,7 @@ import { memo } from 'react'; import styled from 'styled-components'; import breakpoints from '~/components/core/breakpoints'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { BaseM } from '~/components/core/Text/Text'; import { GALLERY_DISCORD, GALLERY_TWITTER } from '~/constants/urls'; @@ -11,6 +11,7 @@ import { GLOBAL_FOOTER_HEIGHT_MOBILE, } from '~/contexts/globalLayout/GlobalFooter/GlobalFooter'; import { LogoLarge } from '~/icons/LogoLarge'; +import { contexts } from '~/shared/analytics/constants'; function MaintenancePage() { return ( @@ -24,9 +25,23 @@ function MaintenancePage() { - Xwitter + + Xwitter + · - Discord + + Discord + ); @@ -54,7 +69,7 @@ const StyledBaseM = styled(BaseM)` text-align: center; `; -const StyledFooterLink = styled(InteractiveLink)` +const StyledFooterLink = styled(GalleryLink)` text-transform: capitalize; font-size: 14px; `; diff --git a/apps/web/src/scenes/MembershipMintPage/CustomizedGeneralMembershipMintPage.tsx b/apps/web/src/scenes/MembershipMintPage/CustomizedGeneralMembershipMintPage.tsx index a94c20a3ec..a5030fcb04 100644 --- a/apps/web/src/scenes/MembershipMintPage/CustomizedGeneralMembershipMintPage.tsx +++ b/apps/web/src/scenes/MembershipMintPage/CustomizedGeneralMembershipMintPage.tsx @@ -8,7 +8,6 @@ import breakpoints, { pageGutter } from '~/components/core/breakpoints'; import { Button } from '~/components/core/Button/Button'; import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import HorizontalBreak from '~/components/core/HorizontalBreak/HorizontalBreak'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; import Markdown from '~/components/core/Markdown/Markdown'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import ErrorText from '~/components/core/Text/ErrorText'; @@ -137,9 +136,9 @@ export function CustomizedGeneralMembershipMintPage({ You are ineligible for this mint. - + - + View on Secondary @@ -157,23 +156,38 @@ export function CustomizedGeneralMembershipMintPage({ Early Access Allowlist can still create a Gallery account. - + - +
    - + ); } return active ? ( - ) : ( - + ); }, [ active, @@ -202,7 +216,7 @@ export function CustomizedGeneralMembershipMintPage({ {membershipNft.title} - + {Number(price) > 0 && ( diff --git a/apps/web/src/scenes/MembershipMintPage/MembershipMintPage.tsx b/apps/web/src/scenes/MembershipMintPage/MembershipMintPage.tsx index 2b349ae983..ce47b6fa23 100644 --- a/apps/web/src/scenes/MembershipMintPage/MembershipMintPage.tsx +++ b/apps/web/src/scenes/MembershipMintPage/MembershipMintPage.tsx @@ -8,7 +8,6 @@ import breakpoints, { pageGutter } from '~/components/core/breakpoints'; import { Button } from '~/components/core/Button/Button'; import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import HorizontalBreak from '~/components/core/HorizontalBreak/HorizontalBreak'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; import Markdown from '~/components/core/Markdown/Markdown'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import ErrorText from '~/components/core/Text/ErrorText'; @@ -143,11 +142,24 @@ export function MembershipMintPage({ } return active ? ( - ) : ( - + ); }, [ active, @@ -175,7 +187,7 @@ export function MembershipMintPage({ {membershipNft.title} - + @@ -210,9 +222,9 @@ export function MembershipMintPage({ You are ineligible for this mint. - + - + )} diff --git a/apps/web/src/scenes/MerchStorePage/Countdown.tsx b/apps/web/src/scenes/MerchStorePage/Countdown.tsx index c3c61c3a04..86767c2968 100644 --- a/apps/web/src/scenes/MerchStorePage/Countdown.tsx +++ b/apps/web/src/scenes/MerchStorePage/Countdown.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import styled from 'styled-components'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { VStack } from '~/components/core/Spacer/Stack'; import { BaseM, BaseS, TitleM } from '~/components/core/Text/Text'; import useTimer from '~/hooks/useTimer'; @@ -62,9 +62,9 @@ export default function Countdown() { {seconds === '1' ? 'sec' : 'secs'} - + See mint schedule - + )} @@ -106,6 +106,6 @@ const StyledCountdownLabel = styled(BaseS)` font-style: normal; `; -const StyledInteractiveLink = styled(InteractiveLink)` +const StyledGalleryLink = styled(GalleryLink)` color: ${colors.metal}; `; diff --git a/apps/web/src/scenes/MerchStorePage/MerchMintButton.tsx b/apps/web/src/scenes/MerchStorePage/MerchMintButton.tsx index 83db52913c..003409a9be 100644 --- a/apps/web/src/scenes/MerchStorePage/MerchMintButton.tsx +++ b/apps/web/src/scenes/MerchStorePage/MerchMintButton.tsx @@ -10,6 +10,7 @@ import { useToastActions } from '~/contexts/toast/ToastContext'; import { useMintMerchContract } from '~/hooks/useContract'; import useMintContractWithQuantity from '~/hooks/useMintContractWithQuantity'; import { useIsMobileOrMobileLargeWindowWidth } from '~/hooks/useWindowSize'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import { MAX_NFTS_PER_WALLET } from './constants'; @@ -57,7 +58,13 @@ export default function MintButton({ onMintSuccess, quantity, tokenId }: Props) return ( <> - + {buttonText} @@ -69,7 +76,12 @@ export default function MintButton({ onMintSuccess, quantity, tokenId }: Props) ? 'Transaction successful!' : 'Transaction submitted. This may take several minutes.'} - + View on Etherscan diff --git a/apps/web/src/scenes/MerchStorePage/MerchStorePage.tsx b/apps/web/src/scenes/MerchStorePage/MerchStorePage.tsx index 73b654fb0e..8f74d8033d 100644 --- a/apps/web/src/scenes/MerchStorePage/MerchStorePage.tsx +++ b/apps/web/src/scenes/MerchStorePage/MerchStorePage.tsx @@ -13,6 +13,7 @@ import { MerchStorePageQueryFragment$key } from '~/generated/MerchStorePageQuery import useAuthModal from '~/hooks/useAuthModal'; import LogoBracketLeft from '~/icons/LogoBracketLeft'; import LogoBracketRight from '~/icons/LogoBracketRight'; +import { contexts } from '~/shared/analytics/constants'; import { useLoggedInUserId } from '~/shared/relay/useLoggedInUserId'; import colors from '~/shared/theme/colors'; @@ -124,7 +125,14 @@ export default function MerchStorePage({ queryRef }: Props) {
    - Redeem + + Redeem + Physical redemption is now available. diff --git a/apps/web/src/scenes/MerchStorePage/PurchaseBox.tsx b/apps/web/src/scenes/MerchStorePage/PurchaseBox.tsx index 05f6f4d55c..444f6b1025 100644 --- a/apps/web/src/scenes/MerchStorePage/PurchaseBox.tsx +++ b/apps/web/src/scenes/MerchStorePage/PurchaseBox.tsx @@ -13,6 +13,7 @@ import { useIsMobileOrMobileLargeWindowWidth } from '~/hooks/useWindowSize'; import CircleMinusIcon from '~/icons/CircleMinusIcon'; import CirclePlusIcon from '~/icons/CirclePlusIcon'; import { DecoratedCloseIcon } from '~/icons/CloseIcon'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import { MAX_NFTS_PER_WALLET } from './constants'; @@ -158,7 +159,14 @@ export default function PurchaseBox({ )} {!soldOut && ( - + Purchase )} @@ -251,6 +259,9 @@ export default function PurchaseBox({ ) ) : ( { setIsReceiptState(false); }} diff --git a/apps/web/src/scenes/MerchStorePage/Redemption/RedeemedPage.tsx b/apps/web/src/scenes/MerchStorePage/Redemption/RedeemedPage.tsx index 6a13ebf3ee..8b19885511 100644 --- a/apps/web/src/scenes/MerchStorePage/Redemption/RedeemedPage.tsx +++ b/apps/web/src/scenes/MerchStorePage/Redemption/RedeemedPage.tsx @@ -8,6 +8,7 @@ import { BaseM } from '~/components/core/Text/Text'; import { useModalActions } from '~/contexts/modal/ModalContext'; import { RedeemedPageFragment$key } from '~/generated/RedeemedPageFragment.graphql'; import ArrowUpRightIcon from '~/icons/ArrowUpRightIcon'; +import { contexts } from '~/shared/analytics/constants'; import { REDEEMED_STATUS } from '../constants'; import { getObjectName } from '../getObjectName'; @@ -64,7 +65,12 @@ export default function RedeemedPage({ merchTokenRefs }: Props) { })} - + Redeem on shopify @@ -76,7 +82,12 @@ export default function RedeemedPage({ merchTokenRefs }: Props) { You have not redeemed any items yet. - + Close diff --git a/apps/web/src/scenes/MerchStorePage/Redemption/ToRedeemPage.tsx b/apps/web/src/scenes/MerchStorePage/Redemption/ToRedeemPage.tsx index 72f6f6bca7..0c381dd9e8 100644 --- a/apps/web/src/scenes/MerchStorePage/Redemption/ToRedeemPage.tsx +++ b/apps/web/src/scenes/MerchStorePage/Redemption/ToRedeemPage.tsx @@ -8,6 +8,7 @@ import { VStack } from '~/components/core/Spacer/Stack'; import { BaseM } from '~/components/core/Text/Text'; import { useModalActions } from '~/contexts/modal/ModalContext'; import { ToRedeemPageFragment$key } from '~/generated/ToRedeemPageFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; import { getObjectName } from '../getObjectName'; import RedeemItem from './RedeemItem'; @@ -106,7 +107,13 @@ export default function ToRedeemPage({ onToggle, merchTokenRefs }: Props) { - + Redeem @@ -117,7 +124,14 @@ export default function ToRedeemPage({ onToggle, merchTokenRefs }: Props) { You do not have any merchandise to redeem. - Close + + Close + )} diff --git a/apps/web/src/scenes/MintPages/MementosPage.tsx b/apps/web/src/scenes/MintPages/MementosPage.tsx index bd16174c83..9af0030b49 100644 --- a/apps/web/src/scenes/MintPages/MementosPage.tsx +++ b/apps/web/src/scenes/MintPages/MementosPage.tsx @@ -1,64 +1,80 @@ import Image from 'next/image'; import { useRouter } from 'next/router'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Route } from 'nextjs-routes'; +import { useMemo, useState } from 'react'; import styled from 'styled-components'; -import { useAccount } from 'wagmi'; import ActionText from '~/components/core/ActionText/ActionText'; import breakpoints, { contentSize } from '~/components/core/breakpoints'; +import { Button } from '~/components/core/Button/Button'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import HorizontalBreak from '~/components/core/HorizontalBreak/HorizontalBreak'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { BaseM, BaseXL, TitleL } from '~/components/core/Text/Text'; -import { OPENSEA_API_BASEURL, OPENSEA_TESTNET_API_BASEURL } from '~/constants/opensea'; import { GALLERY_MEMENTOS_CONTRACT_ADDRESS } from '~/hooks/useContract'; import useTimer from '~/hooks/useTimer'; import { useIsDesktopWindowWidth } from '~/hooks/useWindowSize'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; -import isProduction from '~/utils/isProduction'; -import { MEMENTOS_NFT_TOKEN_ID, MINT_END, MINT_START, pathToImage } from './config'; +import { MINT_END, MINT_START, pathToImage } from './config'; import MintButton from './MintButton'; import useMintPhase from './useMintPhase'; export default function MementosPage() { const isDesktop = useIsDesktopWindowWidth(); - - const { address: rawAddress } = useAccount(); - const address = rawAddress?.toLowerCase(); const [isMinted, setIsMinted] = useState(false); - const openseaBaseUrl = isProduction() ? OPENSEA_API_BASEURL : OPENSEA_TESTNET_API_BASEURL; - - const detectOwnedPosterNftFromOpensea = useCallback( - async (address: string) => { - const response = await fetch( - `${openseaBaseUrl}/api/v1/assets?owner=${address}&asset_contract_addresses=${GALLERY_MEMENTOS_CONTRACT_ADDRESS}&token_ids=${MEMENTOS_NFT_TOKEN_ID}`, - {} - ); - const responseBody = await response.json(); - return responseBody.assets.length > 0; - }, - [openseaBaseUrl] - ); - - useEffect(() => { - async function checkIfMinted(address: string) { - try { - const hasOwnedPosterNft = await detectOwnedPosterNftFromOpensea(address); - setIsMinted(hasOwnedPosterNft); - } catch (_) { - // ignore if ownership check request fails - } - } - - if (address) { - checkIfMinted(address); - } - }, [address, detectOwnedPosterNftFromOpensea]); + // Keeping logic commented in case we want to re-introduce it + + // const { address: rawAddress } = useAccount(); + // const address = rawAddress?.toLowerCase(); + // const openseaBaseUrl = isProduction() ? OPENSEA_API_BASEURL : OPENSEA_TESTNET_API_BASEURL; + + // const detectOwnedPosterNftFromOpensea = useCallback( + // async (address: string) => { + // const response = await fetch( + // `${openseaBaseUrl}/api/v1/assets?owner=${address}&asset_contract_addresses=${GALLERY_MEMENTOS_CONTRACT_ADDRESS}&token_ids=${MEMENTOS_NFT_TOKEN_ID}`, + // {} + // ); + // const responseBody = await response.json(); + // return responseBody.assets.length > 0; + // }, + // [openseaBaseUrl] + // ); + + // useEffect(() => { + // async function checkIfMinted(address: string) { + // try { + // const hasOwnedPosterNft = await detectOwnedPosterNftFromOpensea(address); + // setIsMinted(hasOwnedPosterNft); + // } catch (_) { + // // ignore if ownership check request fails + // } + // } + + // if (address) { + // checkIfMinted(address); + // } + // }, [address, detectOwnedPosterNftFromOpensea]); const { push } = useRouter(); + const shareOnGalleryRoute: Route = useMemo(() => { + return { + pathname: '/home', + query: { + composer: 'true', + tokenId: '0', + contractAddress: GALLERY_MEMENTOS_CONTRACT_ADDRESS, + chain: 'Base', + collection_title: 'Gallery Mementos', + token_title: 'Gallery Mementos: 1K Posts', + caption: 'I just minted Gallery Mementos: 1K Posts on Gallery', + }, + }; + }, []); + return ( @@ -71,20 +87,35 @@ export default function MementosPage() { - Gallery x Base + Gallery Mementos: 1K Posts - Introducing the Gallery x Base memento, a collectible in celebration of Gallery - rolling out support for{' '} - Base Chain. + After months in closed testing, we have crossed 1,000 posts on Gallery and have opened + Posts to the public in Open Beta. This release marks the evolution of Gallery into a + full social app built to share art. A special thank you to our early users and + testers. Learn more about it in our latest{' '} + + blog post + + . You can read more about Gallery Mementos{' '} - + here - + . @@ -93,13 +124,13 @@ export default function MementosPage() { {/*
  • Follow{' '} - @GALLERY{' '} + @GALLERY{' '} on Twitter.
  • */}
  • Minting will open to all from  - August 22nd 1:00PM through August 31st 11:59PM ET + October 19th 9:00AM ET through November 18th 9:00AM ET
  • @@ -119,7 +150,18 @@ export default function MementosPage() { */} {isMinted ? ( - You've succesfully minted this token. + + You've succesfully minted this token! + + + + ) : ( diff --git a/apps/web/src/scenes/MintPages/MintButton.tsx b/apps/web/src/scenes/MintPages/MintButton.tsx index b07cb7c337..99bed5663b 100644 --- a/apps/web/src/scenes/MintPages/MintButton.tsx +++ b/apps/web/src/scenes/MintPages/MintButton.tsx @@ -4,12 +4,17 @@ import useSWR from 'swr'; import { Button } from '~/components/core/Button/Button'; import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; +import { HStack } from '~/components/core/Spacer/Stack'; import ErrorText from '~/components/core/Text/ErrorText'; import { BaseM } from '~/components/core/Text/Text'; import { TransactionStatus } from '~/constants/transaction'; import { useToastActions } from '~/contexts/toast/ToastContext'; import { useMintMementosContract, WagmiContract } from '~/hooks/useContract'; import useMintContract from '~/hooks/useMintContract'; +import CircleMinusIcon from '~/icons/CircleMinusIcon'; +import CirclePlusIcon from '~/icons/CirclePlusIcon'; +import { contexts } from '~/shared/analytics/constants'; +import colors from '~/shared/theme/colors'; import { ALLOWLIST_URL, MEMENTOS_NFT_TOKEN_ID } from './config'; import useMintPhase from './useMintPhase'; @@ -30,6 +35,8 @@ export default function MintButton({ onMintSuccess }: Props) { buttonText: mintButtonText, error, handleClick, + quantity, + setQuantity, } = useMintContract({ contract: contract as WagmiContract | null, tokenId: MEMENTOS_NFT_TOKEN_ID, @@ -60,7 +67,44 @@ export default function MintButton({ onMintSuccess }: Props) { return ( <> - + + + Quantity + + + { + setQuantity(quantity - 1); + }} + disabled={quantity <= 1} + > + + + {quantity} + { + setQuantity(quantity + 1); + }} + > + + + + + + + Total Price + + + {quantity * 0.000777} Ξ + + + {buttonText} {transactionHash && ( @@ -96,3 +140,19 @@ const StyledErrorText = styled(ErrorText)` // prevents long error messages from overflowing word-break: break-word; `; + +const StyledAdjustQuantityButton = styled.button<{ disabled?: boolean }>` + font-size: 16px; + border-radius: 50%; + width: 16px; + height: 16px; + border: 0; + padding: 0; + background: none; + + path { + stroke: ${({ disabled }) => (disabled ? `${colors.porcelain}` : 'auto')}; + } + + cursor: ${({ disabled }) => (disabled ? `default` : 'pointer')}; +`; diff --git a/apps/web/src/scenes/MintPages/PosterPage.tsx b/apps/web/src/scenes/MintPages/PosterPage.tsx index 2f1fc3d4a6..344fbb04f8 100644 --- a/apps/web/src/scenes/MintPages/PosterPage.tsx +++ b/apps/web/src/scenes/MintPages/PosterPage.tsx @@ -1,8 +1,8 @@ import styled from 'styled-components'; import breakpoints, { contentSize, pageGutter } from '~/components/core/breakpoints'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import HorizontalBreak from '~/components/core/HorizontalBreak/HorizontalBreak'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; import { VStack } from '~/components/core/Spacer/Stack'; import { BaseM, TitleM, TitleXS } from '~/components/core/Text/Text'; import { useIsMobileWindowWidth } from '~/hooks/useWindowSize'; @@ -28,8 +28,7 @@ export default function PosterPage() { Thank you for being a member of Gallery. Members celebrated our{' '} - new brand by signing our - poster. + new brand by signing our poster. We made the final poster available to mint as a commemorative token for early diff --git a/apps/web/src/scenes/MintPages/config.ts b/apps/web/src/scenes/MintPages/config.ts index 33aff63dd2..fbc2be2809 100644 --- a/apps/web/src/scenes/MintPages/config.ts +++ b/apps/web/src/scenes/MintPages/config.ts @@ -1,12 +1,12 @@ // time is in EST (GMT-05:00) -export const MINT_START = '2023-08-21T13:00:00-05:00'; -export const MINT_END = '2023-08-31T23:59:00-05:00'; +export const MINT_START = '2023-10-18T09:00:00-05:00'; +export const MINT_END = '2023-11-18T09:00:00-05:00'; // increment this each time we introduce a new token export const MEMENTOS_NFT_TOKEN_ID = 0; // image preview -import featuredImage from 'public/base-gallery-memento.jpg'; +import featuredImage from 'public/1k-posts-memento-min.jpg'; export const pathToImage = featuredImage; // mint page title, description, eligibility criteria are configured directly in `MementosPage.tsx` diff --git a/apps/web/src/scenes/Modals/GenericActionModal.tsx b/apps/web/src/scenes/Modals/GenericActionModal.tsx index b6fa6ff573..c050b0ce5f 100644 --- a/apps/web/src/scenes/Modals/GenericActionModal.tsx +++ b/apps/web/src/scenes/Modals/GenericActionModal.tsx @@ -27,7 +27,15 @@ export default function GenericActionModal({ {bodyText} - {buttonText} + + {buttonText} + ); diff --git a/apps/web/src/scenes/NftDetailPage/LinkToFullPageNftDetailModal.tsx b/apps/web/src/scenes/NftDetailPage/LinkToFullPageNftDetailModal.tsx index 7b31d968c5..51ecf641b2 100644 --- a/apps/web/src/scenes/NftDetailPage/LinkToFullPageNftDetailModal.tsx +++ b/apps/web/src/scenes/NftDetailPage/LinkToFullPageNftDetailModal.tsx @@ -1,14 +1,19 @@ +// eslint-disable-next-line no-restricted-imports import Link from 'next/link'; import { useRouter } from 'next/router'; import { route } from 'nextjs-routes'; -import { ReactNode, useMemo } from 'react'; +import { ReactNode, useCallback, useMemo } from 'react'; import styled from 'styled-components'; +import { GalleryElementTrackingProps, useTrack } from '~/shared/contexts/AnalyticsContext'; +import { normalizeUrl } from '~/utils/normalizeUrl'; + type Props = { username: string; tokenId: string; children?: ReactNode; collectionId?: string; + eventContext: GalleryElementTrackingProps['eventContext']; }; /** @@ -29,6 +34,7 @@ export default function LinkToFullPageNftDetailModal({ username, tokenId, collectionId, + eventContext, }: Props) { const { pathname, query, asPath } = useRouter(); @@ -68,6 +74,20 @@ export default function LinkToFullPageNftDetailModal({ }); }, [collectionId, isCollectionToken, tokenId, username]); + const track = useTrack(); + + // Use manual tracking that imitates `GalleryLink` + const handleClick = useCallback(() => { + track('Link Click', { + to: normalizeUrl({ to: asRoute }), + needsVerification: false, + id: 'Full Page NFT Detail Modal', + name: 'Open Full Page NFT Detail Modal', + context: eventContext, + type: 'internal', + }); + }, [asRoute, eventContext, track]); + return ( {/* NextJS tags don't come with an anchor tag by default, so we're adding one here. This will inherit the `as` URL from the parent component. */} - {children} + + {children} + ); } diff --git a/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftAdditionalDetailsEth.tsx b/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftAdditionalDetailsEth.tsx index 744954f351..cc952acd76 100644 --- a/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftAdditionalDetailsEth.tsx +++ b/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftAdditionalDetailsEth.tsx @@ -5,6 +5,7 @@ import { BaseM, TitleXS } from '~/components/core/Text/Text'; import { EnsOrAddress } from '~/components/EnsOrAddress'; import { LinkableAddress } from '~/components/LinkableAddress'; import { NftAdditionalDetailsEthFragment$key } from '~/generated/NftAdditionalDetailsEthFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; import { hexToDec } from '~/shared/utils/hexToDec'; import NftDetailsExternalLinksEth from './NftDetailsExternalLinksEth'; @@ -42,14 +43,20 @@ export function NftAdditionalDetailsEth({ tokenRef }: NftAdditionaDetailsNonPOAP {token.contract?.creatorAddress?.address && (
    Creator - +
    )} {contract?.contractAddress?.address && (
    Contract address - +
    )} diff --git a/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftAdditionalDetailsTezos.tsx b/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftAdditionalDetailsTezos.tsx index 60faffffcb..7134f20912 100644 --- a/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftAdditionalDetailsTezos.tsx +++ b/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftAdditionalDetailsTezos.tsx @@ -1,19 +1,20 @@ import { graphql, useFragment } from 'react-relay'; import styled from 'styled-components'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import IconContainer from '~/components/core/IconContainer'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { BaseM, TitleXS } from '~/components/core/Text/Text'; import { TitleDiatypeM } from '~/components/core/Text/Text'; +import { GalleryPill } from '~/components/GalleryPill'; import { LinkableAddress } from '~/components/LinkableAddress'; -import { ButtonPill } from '~/components/Pill'; import { TezosDomainOrAddress } from '~/components/TezosDomainOrAddress'; import { NewTooltip } from '~/components/Tooltip/NewTooltip'; import { useTooltipHover } from '~/components/Tooltip/useTooltipHover'; import { NftAdditionalDetailsTezosFragment$key } from '~/generated/NftAdditionalDetailsTezosFragment.graphql'; import { RefreshIcon } from '~/icons/RefreshIcon'; import { useRefreshMetadata } from '~/scenes/NftDetailPage/NftAdditionalDetails/useRefreshMetadata'; +import { contexts } from '~/shared/analytics/constants'; import { extractRelevantMetadataFromToken } from '~/shared/utils/extractRelevantMetadataFromToken'; import { hexToDec } from '~/shared/utils/hexToDec'; @@ -62,14 +63,20 @@ export function NftAdditionalDetailsTezos({ tokenRef }: NftAdditionaDetailsNonPO {token.contract?.creatorAddress?.address && (
    Creator - +
    )} {contract?.contractAddress?.address && (
    Contract address - +
    )} @@ -81,17 +88,45 @@ export function NftAdditionalDetailsTezos({ tokenRef }: NftAdditionaDetailsNonPO )} - {fxhashUrl && View on fx(hash)} - {objktUrl && View on objkt} - {projectUrl && More Info} + {fxhashUrl && ( + + View on fx(hash) + + )} + {objktUrl && ( + + View on objkt + + )} + {projectUrl && ( + + More Info + + )} - - + } /> Refresh metadata @@ -102,7 +137,7 @@ export function NftAdditionalDetailsTezos({ tokenRef }: NftAdditionaDetailsNonPO whiteSpace="pre-line" text={`Last refreshed ${lastUpdated}`} /> - +
    ); } @@ -112,7 +147,3 @@ const StyledLinkContainer = styled.div` flex-direction: column; gap: 4px; `; - -const StartAlignedButtonPill = styled(ButtonPill)` - align-self: start; -`; diff --git a/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftDetailsExternalLinksEth.tsx b/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftDetailsExternalLinksEth.tsx index d343f8798b..878814da9d 100644 --- a/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftDetailsExternalLinksEth.tsx +++ b/apps/web/src/scenes/NftDetailPage/NftAdditionalDetails/NftDetailsExternalLinksEth.tsx @@ -1,15 +1,16 @@ import { graphql, useFragment } from 'react-relay'; import styled from 'styled-components'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import IconContainer from '~/components/core/IconContainer'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { TitleDiatypeM } from '~/components/core/Text/Text'; -import { ButtonPill } from '~/components/Pill'; +import { GalleryPill } from '~/components/GalleryPill'; import { NewTooltip } from '~/components/Tooltip/NewTooltip'; import { useTooltipHover } from '~/components/Tooltip/useTooltipHover'; import { NftDetailsExternalLinksEthFragment$key } from '~/generated/NftDetailsExternalLinksEthFragment.graphql'; import { RefreshIcon } from '~/icons/RefreshIcon'; +import { contexts } from '~/shared/analytics/constants'; import { extractRelevantMetadataFromToken } from '~/shared/utils/extractRelevantMetadataFromToken'; import { useRefreshMetadata } from './useRefreshMetadata'; @@ -41,20 +42,44 @@ export default function NftDetailsExternalLinksEth({ tokenRef }: Props) { return ( - {mirrorUrl && View on Mirror} + {mirrorUrl && ( + + View on Mirror + + )} {prohibitionUrl && ( - View on Prohibition + + View on Prohibition + )} {openseaUrl && ( - View on OpenSea - + View on OpenSea + + - + } /> Refresh metadata @@ -65,16 +90,12 @@ export default function NftDetailsExternalLinksEth({ tokenRef }: Props) { whiteSpace="pre-line" text={`Last refreshed ${lastUpdated}`} /> - + )} - {projectUrl && More Info} + {projectUrl && More Info} ); } const StyledExternalLinks = styled(VStack)``; - -const StartAlignedButtonPill = styled(ButtonPill)` - align-self: start; -`; diff --git a/apps/web/src/scenes/NftDetailPage/NftDetailAsset.test.tsx b/apps/web/src/scenes/NftDetailPage/NftDetailAsset.test.tsx index d02870d778..33abb3c456 100644 --- a/apps/web/src/scenes/NftDetailPage/NftDetailAsset.test.tsx +++ b/apps/web/src/scenes/NftDetailPage/NftDetailAsset.test.tsx @@ -229,6 +229,7 @@ const RetryImageMediaResponse: NftErrorContextRetryMutationMutation = { token: { __typename: 'Token', id: 'Token:testTokenId', + tokenId: 'testTokenId', dbid: 'testTokenId', name: 'Test Token Name', chain: Chain.Ethereum, diff --git a/apps/web/src/scenes/NftDetailPage/NftDetailNote.tsx b/apps/web/src/scenes/NftDetailPage/NftDetailNote.tsx index 2e259da1dc..c886de9645 100644 --- a/apps/web/src/scenes/NftDetailPage/NftDetailNote.tsx +++ b/apps/web/src/scenes/NftDetailPage/NftDetailNote.tsx @@ -10,6 +10,7 @@ import { BaseM, TitleXS } from '~/components/core/Text/Text'; import { AutoResizingTextAreaWithCharCount } from '~/components/core/TextArea/TextArea'; import { GLOBAL_FOOTER_HEIGHT } from '~/contexts/globalLayout/GlobalFooter/GlobalFooter'; import useUpdateNft from '~/hooks/api/tokens/useUpdateNft'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import formatError from '~/shared/errors/formatError'; import unescape from '~/shared/utils/unescape'; @@ -123,7 +124,7 @@ function NoteEditor({ nftCollectorsNote, tokenId, collectionId }: NoteEditorProp footerHeight={GLOBAL_FOOTER_HEIGHT} onDoubleClick={handleEditCollectorsNote} > - + )} {generalError && } @@ -132,6 +133,9 @@ function NoteEditor({ nftCollectorsNote, tokenId, collectionId }: NoteEditorProp {isEditing ? ( MAX_CHAR_COUNT} text="Save Note" onClick={handleSubmitCollectorsNote} @@ -140,6 +144,9 @@ function NoteEditor({ nftCollectorsNote, tokenId, collectionId }: NoteEditorProp ) : ( @@ -171,7 +178,7 @@ export function NoteViewer({ nftCollectorsNote }: NoteViewerProps) { Collector’s Note - + ); diff --git a/apps/web/src/scenes/NftDetailPage/NftDetailText.tsx b/apps/web/src/scenes/NftDetailPage/NftDetailText.tsx index dc9abc7067..a293dc6833 100644 --- a/apps/web/src/scenes/NftDetailPage/NftDetailText.tsx +++ b/apps/web/src/scenes/NftDetailPage/NftDetailText.tsx @@ -5,14 +5,14 @@ import styled from 'styled-components'; import breakpoints, { size } from '~/components/core/breakpoints'; import { Button } from '~/components/core/Button/Button'; import TextButton from '~/components/core/Button/TextButton'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import HorizontalBreak from '~/components/core/HorizontalBreak/HorizontalBreak'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; import Markdown from '~/components/core/Markdown/Markdown'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { BaseM, TitleDiatypeM, TitleM, TitleXS } from '~/components/core/Text/Text'; +import { GalleryPill } from '~/components/GalleryPill'; import CommunityHoverCard from '~/components/HoverCard/CommunityHoverCard'; import UserHoverCard from '~/components/HoverCard/UserHoverCard'; -import { ClickablePill, NonclickablePill } from '~/components/Pill'; import { PostComposerModal } from '~/components/Posts/PostComposerModal'; import { ProfilePicture } from '~/components/ProfilePicture/ProfilePicture'; import { ProfilePictureStack } from '~/components/ProfilePicture/ProfilePictureStack'; @@ -28,6 +28,7 @@ import { useBreakpoint, useIsMobileWindowWidth } from '~/hooks/useWindowSize'; import { PlusSquareIcon } from '~/icons/PlusSquareIcon'; import { AdmireIcon } from '~/icons/SocializeIcons'; import { NftAdditionalDetails } from '~/scenes/NftDetailPage/NftAdditionalDetails/NftAdditionalDetails'; +import { contexts, flows } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import { removeNullValues } from '~/shared/relay/removeNullValues'; import colors from '~/shared/theme/colors'; @@ -219,21 +220,6 @@ function NftDetailText({ queryRef, tokenRef, authenticatedUserOwnsAsset }: Props openseaUrl, ]); - const handleCollectorNameClick = useCallback(() => { - track('NFT Detail Collector Name Click', { - username: token.owner?.username ? token.owner.username.toLowerCase() : undefined, - contractAddress: token.contract?.contractAddress?.address, - tokenId: token.tokenId, - externaUrl: openseaUrl, - }); - }, [ - track, - token.owner?.username, - token.contract?.contractAddress?.address, - token.tokenId, - openseaUrl, - ]); - const communityUrl = getCommunityUrlForToken(token); const metadata = JSON.parse(token.tokenMetadata ?? '{}') ?? {}; @@ -250,13 +236,17 @@ function NftDetailText({ queryRef, tokenRef, authenticatedUserOwnsAsset }: Props }, [token.name]); const handleCreatePostClick = useCallback(() => { - track('NFT Detail: Clicked Create Post'); showModal({ - content: , + content: ( + + ), headerVariant: 'thicc', isFullPage: isMobile, }); - }, [isMobile, showModal, token, track]); + }, [isMobile, showModal, token]); return ( @@ -279,18 +269,28 @@ function NftDetailText({ queryRef, tokenRef, authenticatedUserOwnsAsset }: Props {communityUrl && token.community ? ( - + {token.chain === 'POAP' && } {token.contract?.badgeURL && } {contractName} - + ) : ( - + {contractName} - + )}
    @@ -300,15 +300,10 @@ function NftDetailText({ queryRef, tokenRef, authenticatedUserOwnsAsset }: Props {token.ownerIsCreator ? 'CREATOR' : 'OWNER'} - - - - {token.owner.username} - - + + + {token.owner.username} + )} @@ -316,25 +311,25 @@ function NftDetailText({ queryRef, tokenRef, authenticatedUserOwnsAsset }: Props // TODO: Update this to use the creator's username CREATOR - riley.eth - - + peterson.eth - + )}
    {token.description && ( - + )} @@ -345,14 +340,19 @@ function NftDetailText({ queryRef, tokenRef, authenticatedUserOwnsAsset }: Props {SHOW_BUY_NOW_BUTTON && ( - - Buy Now - + + + Buy Now + + )} ) : null} - {authenticatedUserOwnsAsset && ( - + Create Post @@ -380,15 +384,32 @@ function NftDetailText({ queryRef, tokenRef, authenticatedUserOwnsAsset }: Props )} + {poapMoreInfoUrl || poapUrl ? ( - {poapMoreInfoUrl && More Info} - {poapUrl && View on POAP} + {poapMoreInfoUrl && More Info} + {poapUrl && View on POAP} ) : null} - {!showDetails && } - {showDetails && } + {!showDetails && ( + + )} + {showDetails && ( + + )} ); @@ -427,7 +448,7 @@ const StyledDetailLabel = styled.div<{ horizontalLayout: boolean; navbarHeight: } `; -const StyledInteractiveLink = styled(InteractiveLink)` +const StyledGalleryLink = styled(GalleryLink)` text-decoration: none; `; diff --git a/apps/web/src/scenes/NotFound/NotFound.tsx b/apps/web/src/scenes/NotFound/NotFound.tsx index 6da5a92d2a..e644178fc7 100644 --- a/apps/web/src/scenes/NotFound/NotFound.tsx +++ b/apps/web/src/scenes/NotFound/NotFound.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { ButtonLink } from '~/components/core/Button/Button'; +import { DeprecatedButtonLink } from '~/components/core/Button/Button'; import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { VStack } from '~/components/core/Spacer/Stack'; import { BaseM, TitleL } from '~/components/core/Text/Text'; @@ -21,7 +21,7 @@ function NotFound({ resource = 'user' }: Props) { Discord. - Take me back + Take me back ); } diff --git a/apps/web/src/scenes/Nuke/Nuke.tsx b/apps/web/src/scenes/Nuke/Nuke.tsx index 96a3fd14a2..d010d1c779 100644 --- a/apps/web/src/scenes/Nuke/Nuke.tsx +++ b/apps/web/src/scenes/Nuke/Nuke.tsx @@ -1,7 +1,7 @@ import { memo, useEffect } from 'react'; import styled from 'styled-components'; -import { ButtonLink } from '~/components/core/Button/Button'; +import { DeprecatedButtonLink } from '~/components/core/Button/Button'; import { VStack } from '~/components/core/Spacer/Stack'; import { BaseXL } from '~/components/core/Text/Text'; import { useLogout } from '~/hooks/useLogout'; @@ -18,7 +18,7 @@ function Nuke() { return ( Your local cache has been nuked - Take me home + Take me home ); } diff --git a/apps/web/src/scenes/UserGalleryPage/EditUserInfoModal.tsx b/apps/web/src/scenes/UserGalleryPage/EditUserInfoModal.tsx index f413105366..2f2f0d6748 100644 --- a/apps/web/src/scenes/UserGalleryPage/EditUserInfoModal.tsx +++ b/apps/web/src/scenes/UserGalleryPage/EditUserInfoModal.tsx @@ -11,6 +11,7 @@ import EditUserInfoForm from '~/components/Profile/EditUserInfoForm'; import useUserInfoForm from '~/components/Profile/useUserInfoForm'; import { useModalActions } from '~/contexts/modal/ModalContext'; import { EditUserInfoModalFragment$key } from '~/generated/EditUserInfoModalFragment.graphql'; +import { contexts, flows } from '~/shared/analytics/constants'; type Props = { queryRef: EditUserInfoModalFragment$key; @@ -94,7 +95,15 @@ function EditUserInfoModal({ queryRef }: Props) { {/* TODO [GAL-256]: This spacer and button should be part of a new ModalFooter */} - diff --git a/apps/web/src/scenes/UserGalleryPage/EmptyGallery.tsx b/apps/web/src/scenes/UserGalleryPage/EmptyGallery.tsx index 24bb553c40..e27d7e58c0 100644 --- a/apps/web/src/scenes/UserGalleryPage/EmptyGallery.tsx +++ b/apps/web/src/scenes/UserGalleryPage/EmptyGallery.tsx @@ -7,6 +7,7 @@ import breakpoints from '~/components/core/breakpoints'; import { Button } from '~/components/core/Button/Button'; import { VStack } from '~/components/core/Spacer/Stack'; import { TitleM } from '~/components/core/Text/Text'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; type EmptyGalleryProps = { @@ -50,7 +51,14 @@ export function EmptyAuthenticatedUsersGallery({ galleryId }: EmptyAuthenticated Get started below and showcase your collection. - + ); } diff --git a/apps/web/src/scenes/UserGalleryPage/GalleryNameDescriptionHeader.tsx b/apps/web/src/scenes/UserGalleryPage/GalleryNameDescriptionHeader.tsx index 1a368ae53b..5b13eaea85 100644 --- a/apps/web/src/scenes/UserGalleryPage/GalleryNameDescriptionHeader.tsx +++ b/apps/web/src/scenes/UserGalleryPage/GalleryNameDescriptionHeader.tsx @@ -1,5 +1,4 @@ -import Link from 'next/link'; -import { Route, route } from 'nextjs-routes'; +import { Route } from 'nextjs-routes'; import { useMemo } from 'react'; import { useFragment } from 'react-relay'; import { graphql } from 'relay-runtime'; @@ -7,11 +6,13 @@ import styled from 'styled-components'; import breakpoints from '~/components/core/breakpoints'; import { DisplayLayout } from '~/components/core/enums'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import Markdown from '~/components/core/Markdown/Markdown'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { BaseM, TitleM } from '~/components/core/Text/Text'; import { GalleryNameDescriptionHeaderFragment$key } from '~/generated/GalleryNameDescriptionHeaderFragment.graphql'; import { useIsMobileWindowWidth } from '~/hooks/useWindowSize'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; import unescape from '~/shared/utils/unescape'; @@ -77,13 +78,18 @@ function GalleryNameDescriptionHeader({ return ( - - {noLink ? ( - galleryName - ) : ( - {galleryName} - )} - + {noLink ? ( + galleryName + ) : ( + + {galleryName} + + )} {showMobileLayoutToggle && ( @@ -95,7 +101,7 @@ function GalleryNameDescriptionHeader({ - + @@ -120,11 +126,6 @@ const Container = styled(VStack)` width: 100%; `; -const GalleryLink = styled.a` - all: unset; - cursor: pointer; -`; - const StyledButtonsWrapper = styled(HStack)` height: 36px; @media only screen and ${breakpoints.mobile} { diff --git a/apps/web/src/scenes/UserGalleryPage/GalleryTitleBreadcrumb.tsx b/apps/web/src/scenes/UserGalleryPage/GalleryTitleBreadcrumb.tsx index 16504849bd..d67949e853 100644 --- a/apps/web/src/scenes/UserGalleryPage/GalleryTitleBreadcrumb.tsx +++ b/apps/web/src/scenes/UserGalleryPage/GalleryTitleBreadcrumb.tsx @@ -1,16 +1,17 @@ -import Link from 'next/link'; import { useRouter } from 'next/router'; -import { Route, route } from 'nextjs-routes'; +import { Route } from 'nextjs-routes'; import { useMemo } from 'react'; import { graphql, useFragment } from 'react-relay'; import styled from 'styled-components'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { HStack } from '~/components/core/Spacer/Stack'; import { BreadcrumbLink, BreadcrumbText, } from '~/contexts/globalLayout/GlobalNavbar/ProfileDropdown/Breadcrumbs'; import { GalleryTitleBreadcrumbFragment$key } from '~/generated/GalleryTitleBreadcrumbFragment.graphql'; +import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; type Props = { @@ -46,24 +47,29 @@ export default function GalleryTitleBreadcrumb({ username, galleryRef }: Props) {!isHome && ( <> - - - {username} - - + + {username} +  / )} {gallery.name && ( - - + + {gallery.name} - + )} ); diff --git a/apps/web/src/scenes/UserGalleryPage/UserFarcasterSection.tsx b/apps/web/src/scenes/UserGalleryPage/UserFarcasterSection.tsx index 2c25ce8e66..73c6a88947 100644 --- a/apps/web/src/scenes/UserGalleryPage/UserFarcasterSection.tsx +++ b/apps/web/src/scenes/UserGalleryPage/UserFarcasterSection.tsx @@ -32,6 +32,11 @@ export default function UserFarcasterSection({ userRef }: Props) { const farcasterUrl = `https://warpcast.com/${farcasterUsername}`; return ( - } username={farcasterUsername} /> + } + username={farcasterUsername} + platform="farcaster" + /> ); } diff --git a/apps/web/src/scenes/UserGalleryPage/UserGalleryCollection.tsx b/apps/web/src/scenes/UserGalleryPage/UserGalleryCollection.tsx index 20543463ef..11a7e1ad5e 100644 --- a/apps/web/src/scenes/UserGalleryPage/UserGalleryCollection.tsx +++ b/apps/web/src/scenes/UserGalleryPage/UserGalleryCollection.tsx @@ -24,7 +24,7 @@ import { UserGalleryCollectionFragment$key } from '~/generated/UserGalleryCollec import { UserGalleryCollectionQueryFragment$key } from '~/generated/UserGalleryCollectionQueryFragment.graphql'; import useUpdateCollectionInfo from '~/hooks/api/collections/useUpdateCollectionInfo'; import useResizeObserver from '~/hooks/useResizeObserver'; -import { useTrack } from '~/shared/contexts/AnalyticsContext'; +import { contexts } from '~/shared/analytics/constants'; import { useLoggedInUserId } from '~/shared/relay/useLoggedInUserId'; import unescape from '~/shared/utils/unescape'; import { getBaseUrl } from '~/utils/getBaseUrl'; @@ -96,8 +96,6 @@ function UserGalleryCollection({ }; const collectionUrl = route(collectionUrlPath); - const track = useTrack(); - // Get height of this component const componentRef = useRef(null); const { height: collectionElHeight } = useResizeObserver(componentRef); @@ -109,10 +107,6 @@ function UserGalleryCollection({ } }, [collectionElHeight, onLoad, cacheHeight]); - const handleShareClick = useCallback(() => { - track('Share Collection', { path: collectionUrl }); - }, [track, collectionUrl]); - const [updateCollectionInfo] = useUpdateCollectionInfo(); const handleEditNameClick = useCallback(() => { showModal({ @@ -145,29 +139,40 @@ function UserGalleryCollection({ - + {showEditActions && ( <> - - Edit Name & Description - + track('Update existing collection button clicked')} - > - Edit Collection - + name="Manage Collection" + eventContext={contexts.UserGallery} + label="Edit Collection" + /> )} - - View Collection - + @@ -175,7 +180,7 @@ function UserGalleryCollection({ {unescapedCollectorsNote && ( <> - + )} diff --git a/apps/web/src/scenes/UserGalleryPage/UserLensSection.tsx b/apps/web/src/scenes/UserGalleryPage/UserLensSection.tsx index bc1c09689f..00b3511728 100644 --- a/apps/web/src/scenes/UserGalleryPage/UserLensSection.tsx +++ b/apps/web/src/scenes/UserGalleryPage/UserLensSection.tsx @@ -37,5 +37,7 @@ export default function UserLensSection({ userRef }: Props) { const lensUrl = `https://lenster.xyz/u/${rawLensUsername}`; - return } username={rawLensUsername} />; + return ( + } username={rawLensUsername} platform="lens" /> + ); } diff --git a/apps/web/src/scenes/UserGalleryPage/UserNameAndDescriptionHeader.tsx b/apps/web/src/scenes/UserGalleryPage/UserNameAndDescriptionHeader.tsx index 75b5bb8153..0a1f198043 100644 --- a/apps/web/src/scenes/UserGalleryPage/UserNameAndDescriptionHeader.tsx +++ b/apps/web/src/scenes/UserGalleryPage/UserNameAndDescriptionHeader.tsx @@ -6,8 +6,8 @@ import styled, { css } from 'styled-components'; import Badge from '~/components/Badge/Badge'; import breakpoints from '~/components/core/breakpoints'; import TextButton from '~/components/core/Button/TextButton'; +import { StyledAnchor } from '~/components/core/GalleryLink/GalleryLink'; import IconContainer from '~/components/core/IconContainer'; -import { StyledAnchor } from '~/components/core/InteractiveLink/InteractiveLink'; import Markdown from '~/components/core/Markdown/Markdown'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { BaseM, TitleM } from '~/components/core/Text/Text'; @@ -18,6 +18,7 @@ import { UserNameAndDescriptionHeaderQueryFragment$key } from '~/generated/UserN import useIs3acProfilePage from '~/hooks/oneOffs/useIs3acProfilePage'; import { useIsMobileWindowWidth } from '~/hooks/useWindowSize'; import { EditPencilIcon } from '~/icons/EditPencilIcon'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import { useLoggedInUserId } from '~/shared/relay/useLoggedInUserId'; import colors from '~/shared/theme/colors'; @@ -107,13 +108,15 @@ export function UserNameAndDescriptionHeader({ userRef, queryRef }: Props) { - + {displayName} {userBadges.map((badge) => - badge ? : null + badge ? ( + + ) : null )} @@ -124,7 +127,7 @@ export function UserNameAndDescriptionHeader({ userRef, queryRef }: Props) { ) : ( - + )} @@ -160,9 +163,18 @@ const ExpandableBio = ({ text }: { text: string }) => { - {isExpanded ? null : } + {isExpanded ? null : ( + + )} ); }; @@ -184,7 +196,12 @@ const NftDetailViewer = ({ href, children }: NftDetailViewerProps) => { } return ( - + {children} ); diff --git a/apps/web/src/scenes/UserGalleryPage/UserSharedInfo/UserSharedCommunities.tsx b/apps/web/src/scenes/UserGalleryPage/UserSharedInfo/UserSharedCommunities.tsx index 1845f4257e..8b2fe91ba7 100644 --- a/apps/web/src/scenes/UserGalleryPage/UserSharedInfo/UserSharedCommunities.tsx +++ b/apps/web/src/scenes/UserGalleryPage/UserSharedInfo/UserSharedCommunities.tsx @@ -2,13 +2,14 @@ import { useCallback, useMemo } from 'react'; import { graphql, useFragment } from 'react-relay'; import styled from 'styled-components'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { HStack } from '~/components/core/Spacer/Stack'; import { BaseS } from '~/components/core/Text/Text'; import { CommunityProfilePictureStack } from '~/components/ProfilePicture/CommunityProfilePictureStack'; import { useModalActions } from '~/contexts/modal/ModalContext'; import { UserSharedCommunitiesFragment$key } from '~/generated/UserSharedCommunitiesFragment.graphql'; import { useIsMobileWindowWidth } from '~/hooks/useWindowSize'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import { removeNullValues } from '~/shared/relay/removeNullValues'; import { LowercaseChain } from '~/shared/utils/chains'; @@ -91,9 +92,16 @@ export default function UserSharedCommunities({ userRef }: Props) { if (url) { return ( - + {community.name} - + ); } } @@ -103,9 +111,15 @@ export default function UserSharedCommunities({ userRef }: Props) { // If there are more than 3 communities, add a link to show all in a popover if (totalSharedCommunities > 3) { result.push( - + {totalSharedCommunities - 2} others - + ); } @@ -139,7 +153,7 @@ export default function UserSharedCommunities({ userRef }: Props) { ); } -const StyledInteractiveLink = styled(InteractiveLink)` +const StyledGalleryLink = styled(GalleryLink)` font-size: 12px; `; diff --git a/apps/web/src/scenes/UserGalleryPage/UserSharedInfo/UserSharedFollowers.tsx b/apps/web/src/scenes/UserGalleryPage/UserSharedInfo/UserSharedFollowers.tsx index 6fa6cdf82a..a153fbd62b 100644 --- a/apps/web/src/scenes/UserGalleryPage/UserSharedInfo/UserSharedFollowers.tsx +++ b/apps/web/src/scenes/UserGalleryPage/UserSharedInfo/UserSharedFollowers.tsx @@ -2,13 +2,14 @@ import { useCallback, useMemo } from 'react'; import { graphql, useFragment } from 'react-relay'; import styled from 'styled-components'; -import InteractiveLink from '~/components/core/InteractiveLink/InteractiveLink'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import { HStack } from '~/components/core/Spacer/Stack'; import { BaseS } from '~/components/core/Text/Text'; import { ProfilePictureStack } from '~/components/ProfilePicture/ProfilePictureStack'; import { useModalActions } from '~/contexts/modal/ModalContext'; import { UserSharedFollowersFragment$key } from '~/generated/UserSharedFollowersFragment.graphql'; import { useIsMobileWindowWidth } from '~/hooks/useWindowSize'; +import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import { removeNullValues } from '~/shared/relay/removeNullValues'; @@ -77,23 +78,33 @@ export default function UserSharedFollowers({ userRef }: Props) { const content = useMemo(() => { // Display up to 3 usernames const result = followersToDisplay.map((user) => ( - {user.username} - + )); // If there are more than 3 usernames, add a link to show all in a popover if (totalSharedFollowers > 3) { result.push( - + {totalSharedFollowers - 2} others - + ); } @@ -127,7 +138,7 @@ export default function UserSharedFollowers({ userRef }: Props) { ); } -const StyledInteractiveLink = styled(InteractiveLink)` +const StyledGalleryLink = styled(GalleryLink)` font-size: 12px; `; diff --git a/apps/web/src/scenes/UserGalleryPage/UserSharedInfo/UserSharedInfoList/SharedInfoListRow.tsx b/apps/web/src/scenes/UserGalleryPage/UserSharedInfo/UserSharedInfoList/SharedInfoListRow.tsx index 7409acd68d..4fce0f147f 100644 --- a/apps/web/src/scenes/UserGalleryPage/UserSharedInfo/UserSharedInfoList/SharedInfoListRow.tsx +++ b/apps/web/src/scenes/UserGalleryPage/UserSharedInfo/UserSharedInfoList/SharedInfoListRow.tsx @@ -1,8 +1,8 @@ -import Link from 'next/link'; import { Route } from 'nextjs-routes'; import { useCallback, useMemo } from 'react'; import styled from 'styled-components'; +import GalleryLink from '~/components/core/GalleryLink/GalleryLink'; import Markdown from '~/components/core/Markdown/Markdown'; import { HStack, VStack } from '~/components/core/Spacer/Stack'; import { BaseM, TitleDiatypeM } from '~/components/core/Text/Text'; @@ -26,7 +26,11 @@ export default function SharedInfoListRow({ title, subTitle, href, imageContent {title} {subTitle && ( - + )} @@ -42,7 +46,14 @@ export default function SharedInfoListRow({ title, subTitle, href, imageContent } return ( - + {rowContent} ); @@ -55,7 +66,7 @@ const StyledRowNonLink = styled.div` } `; -const StyledRowLink = styled(Link)` +const StyledRowLink = styled(GalleryLink)` padding: 8px 12px; text-decoration: none; max-height: 56px; diff --git a/apps/web/src/scenes/UserGalleryPage/UserSocialPill.tsx b/apps/web/src/scenes/UserGalleryPage/UserSocialPill.tsx index 59badf6f06..45481f779e 100644 --- a/apps/web/src/scenes/UserGalleryPage/UserSocialPill.tsx +++ b/apps/web/src/scenes/UserGalleryPage/UserSocialPill.tsx @@ -2,18 +2,27 @@ import styled from 'styled-components'; import { HStack } from '~/components/core/Spacer/Stack'; import { TitleXS } from '~/components/core/Text/Text'; -import { ClickablePill } from '~/components/Pill'; +import { GalleryPill } from '~/components/GalleryPill'; +import { contexts } from '~/shared/analytics/constants'; type Props = { url: string; icon: React.ReactNode; username: string; className?: string; + platform: 'twitter' | 'lens' | 'farcaster'; }; -export default function UserSocialPill({ url, icon, username, className }: Props) { +export default function UserSocialPill({ url, icon, username, className, platform }: Props) { return ( - + {icon} {username} @@ -22,13 +31,14 @@ export default function UserSocialPill({ url, icon, username, className }: Props ); } -export const StyledUserSocialPill = styled(ClickablePill)` +export const StyledUserSocialPill = styled(GalleryPill)` flex-basis: 0; `; const StyledPillContent = styled(HStack)` width: 100%; `; + const StyledIconContainer = styled.div` width: 16px; display: flex; diff --git a/apps/web/src/scenes/UserGalleryPage/UserTwitterSection.tsx b/apps/web/src/scenes/UserGalleryPage/UserTwitterSection.tsx index 9993d7bd95..711b2a4e6d 100644 --- a/apps/web/src/scenes/UserGalleryPage/UserTwitterSection.tsx +++ b/apps/web/src/scenes/UserGalleryPage/UserTwitterSection.tsx @@ -1,11 +1,12 @@ import { graphql, useFragment } from 'react-relay'; import { HStack } from '~/components/core/Spacer/Stack'; -import { ClickablePill } from '~/components/Pill'; +import { GalleryPill } from '~/components/GalleryPill'; import { TWITTER_AUTH_URL } from '~/constants/twitter'; import { UserTwitterSectionFragment$key } from '~/generated/UserTwitterSectionFragment.graphql'; import { UserTwitterSectionQueryFragment$key } from '~/generated/UserTwitterSectionQueryFragment.graphql'; import TwitterIcon from '~/icons/TwitterIcon'; +import { contexts, flows } from '~/shared/analytics/constants'; import { useLoggedInUserId } from '~/shared/relay/useLoggedInUserId'; import UserSocialPill from './UserSocialPill'; @@ -50,12 +51,19 @@ export default function UserTwitterSection({ queryRef, userRef }: Props) { if (isAuthenticatedUser && !userTwitterAccount) { return ( - + Connect Twitter - + ); } @@ -71,6 +79,7 @@ export default function UserTwitterSection({ queryRef, userRef }: Props) { url={twitterUrl} icon={} username={userTwitterAccount.username} + platform="twitter" /> ); } diff --git a/apps/web/src/scenes/WelcomeAnimation/WelcomeAnimation.tsx b/apps/web/src/scenes/WelcomeAnimation/WelcomeAnimation.tsx index 739cccd955..fc65fb783a 100644 --- a/apps/web/src/scenes/WelcomeAnimation/WelcomeAnimation.tsx +++ b/apps/web/src/scenes/WelcomeAnimation/WelcomeAnimation.tsx @@ -106,7 +106,15 @@ export default function WelcomeAnimation() { arrange, and display your collection exactly how it was meant to be. - Enter Gallery + + Enter Gallery + {animatedImages.map((animatedImage) => ( diff --git a/apps/web/src/utils/logger.ts b/apps/web/src/utils/logger.ts new file mode 100644 index 0000000000..5a34fb98d7 --- /dev/null +++ b/apps/web/src/utils/logger.ts @@ -0,0 +1,10 @@ +import { getTimestamp } from '~/shared/utils/time'; + +import isProduction from './isProduction'; + +export const _log: Console['log'] = (...args) => { + if (isProduction()) { + return; + } + return console.log(`%c[${getTimestamp()}]`, 'color:#4878fa', `${args[0]}`, ...args.slice(1)); +}; diff --git a/apps/web/src/utils/normalizeUrl.test.ts b/apps/web/src/utils/normalizeUrl.test.ts new file mode 100644 index 0000000000..abefbcaf87 --- /dev/null +++ b/apps/web/src/utils/normalizeUrl.test.ts @@ -0,0 +1,45 @@ +import { Route } from 'nextjs-routes'; + +import { normalizeUrl } from './normalizeUrl'; + +describe('normalizeUrl', () => { + test('should handle string external URLs', () => { + expect(normalizeUrl({ to: 'https://google.com' })).toBe('https://google.com'); + expect(normalizeUrl({ to: 'https://prohibition.art/contract/1' })).toBe( + 'https://prohibition.art/contract/1' + ); + expect(normalizeUrl({ href: 'https://google.com' })).toBe('https://google.com'); + expect(normalizeUrl({ href: 'https://prohibition.art/contract/1' })).toBe( + 'https://prohibition.art/contract/1' + ); + }); + + test('should normalize string internal URLs', () => { + expect(normalizeUrl({ to: '/bingbong' })).toBe('https://gallery.so/bingbong'); + expect(normalizeUrl({ to: '/community/12345' })).toBe('https://gallery.so/community/12345'); + + expect(normalizeUrl({ href: '/bingbong' })).toBe('https://gallery.so/bingbong'); + expect(normalizeUrl({ href: '/community/12345' })).toBe('https://gallery.so/community/12345'); + }); + + test('should normalize internal Route objects', () => { + const userRoute: Route = { + pathname: '/[username]', + query: { username: 'bingbong' }, + }; + + const communityRoute: Route = { + pathname: '/community/[chain]/[contractAddress]', + query: { + contractAddress: '12345' as string, + chain: 'Ethereum' as string, + }, + }; + + expect(normalizeUrl({ to: userRoute })).toBe('https://gallery.so/bingbong'); + + expect(normalizeUrl({ to: communityRoute })).toBe( + 'https://gallery.so/community/Ethereum/12345' + ); + }); +}); diff --git a/apps/web/src/utils/normalizeUrl.ts b/apps/web/src/utils/normalizeUrl.ts new file mode 100644 index 0000000000..6aba06bd90 --- /dev/null +++ b/apps/web/src/utils/normalizeUrl.ts @@ -0,0 +1,19 @@ +import { Route, route } from 'nextjs-routes'; + +export function normalizeUrl({ to, href }: { to?: Route | string; href?: string }) { + let url = ''; + if (to) { + if (typeof to === 'string') { + url = to; + } else { + url = route(to); + } + } + if (href) { + url = href; + } + if (url.startsWith('/')) { + url = `https://gallery.so${url}`; + } + return url; +} diff --git a/apps/web/tests/graphql/mockGlobalLayoutQuery.ts b/apps/web/tests/graphql/mockGlobalLayoutQuery.ts index 068b47e80d..462f51059d 100644 --- a/apps/web/tests/graphql/mockGlobalLayoutQuery.ts +++ b/apps/web/tests/graphql/mockGlobalLayoutQuery.ts @@ -13,8 +13,9 @@ export function mockGlobalLayoutQuery() { user: { __typename: 'GalleryUser', id: GALLERY_USER_ID, - // @ts-expect-error not sure what the issue is username: 'Test Gallery User', + // @ts-expect-error not sure what the issue is + userExperiences: [], wallets: [], roles: ['ADMIN'], primaryWallet: { diff --git a/packages/shared/src/analytics/constants.ts b/packages/shared/src/analytics/constants.ts new file mode 100644 index 0000000000..bc189997e2 --- /dev/null +++ b/packages/shared/src/analytics/constants.ts @@ -0,0 +1,53 @@ +// higher level categories +export const contexts = { + Posts: 'Posts', + Feed: 'Feed', + Explore: 'Explore', + Editor: 'Editor', + Social: 'Social', + Notifications: 'Notifications', + Authentication: 'Authentication', + 'Manage Wallets': 'Manage Wallets', + Onboarding: 'Onboarding', + Email: 'Email', + Settings: 'Settings', + 'External Social': 'External Social', + UserCollection: 'UserCollection', + UserGallery: 'UserGallery', + 'NFT Detail': 'NFT Detail', + Community: 'Community', + Search: 'Search', + 'Global Banner': 'Global Banner', + 'Global Announcement Popover': 'Global Announcement Popover', + 'Mobile App Upsell': 'Mobile App Upsell', + Mementos: 'Mementos', + 'Merch Store': 'Merch Store', + 'Hover Card': 'Hover Card', + Changelog: 'Changelog', + Error: 'Error', + Maintenance: 'Maintenance', + PFP: 'PFP', + Toast: 'Toast', + Navigation: 'Navigation', + 'Push Notifications': 'Push Notifications', +} as const; + +export type AnalyticsEventContextType = keyof typeof contexts; + +// the specific feature; the name of the flow should give you +// an instant visual of the steps for that flow +export const flows = { + 'Web Signup Flow': 'Web Signup Flow', + 'Mobile Login Flow': 'Mobile Login Flow', + 'Web Sign Out Flow': 'Web Sign Out Flow', + Twitter: 'Twitter', + 'Edit User Info': 'Edit User Info', + 'Posts Beta Announcement': 'Posts Beta Announcement', + 'Share To Gallery': 'Share To Gallery', + 'Web Notifications Post Create Flow': 'Web Notifications Post Create Flow', + 'Web Sidebar Post Create Flow': 'Web Sidebar Post Create Flow', + 'Community Page Post Create Flow': 'Community Page Post Create Flow', + 'NFT Detail Page Post Create Flow': 'NFT Detail Page Post Create Flow', +} as const; + +export type AnalyticsEventFlowType = keyof typeof flows; diff --git a/packages/shared/src/components/GalleryProccessedText/GalleryProcessedText.tsx b/packages/shared/src/components/GalleryProccessedText/GalleryProcessedText.tsx new file mode 100644 index 0000000000..673e010e46 --- /dev/null +++ b/packages/shared/src/components/GalleryProccessedText/GalleryProcessedText.tsx @@ -0,0 +1,120 @@ +import { ReactNode, useMemo } from 'react'; +import { graphql, useFragment } from 'react-relay'; + +import { GalleryProcessedTextFragment$key } from '~/generated/GalleryProcessedTextFragment.graphql'; + +import { + getMarkdownLinkElements, + getMentionElements, + getUrlElements, + TextElement, +} from './GalleryTextElementParser'; +import { SupportedProcessedTextElements } from './types'; + +type GalleryProcessedTextProps = { + text: string; + mentionsRef?: GalleryProcessedTextFragment$key; +} & SupportedProcessedTextElements; + +// Makes a raw text value display-ready by converting urls to link components +export default function GalleryProcessedText({ + text, + mentionsRef = [], + BreakComponent, + TextComponent, + LinkComponent, + MentionComponent, +}: GalleryProcessedTextProps) { + const mentions = useFragment( + graphql` + fragment GalleryProcessedTextFragment on Mention @relay(plural: true) { + __typename + ...GalleryTextElementParserMentionsFragment + } + `, + mentionsRef + ); + + const processedText = useMemo(() => { + const elementsWithBreaks: JSX.Element[] = []; + const markdownLinks = getMarkdownLinkElements(text); + const elements = [ + ...markdownLinks, + ...getUrlElements(text, markdownLinks), + ...getMentionElements(text, mentions), + ]; + + // Sort elements based on their start index + elements.sort((a, b) => a.start - b.start); + + let lastEndIndex = 0; + elements.forEach((element, index) => { + // Split any text containing newlines into parts, inserting BreakComponent between parts + const textSegment = text.substring(lastEndIndex, element.start); + const textParts = textSegment.split('\n'); + textParts.forEach((part, partIndex) => { + if (part) + elementsWithBreaks.push( + {part} + ); + if (partIndex < textParts.length - 1) + elementsWithBreaks.push(); + }); + + // Add the element (either mention, URL, or markdown-link) + addLinkElement({ + result: elementsWithBreaks, + element, + index, + LinkComponent, + MentionComponent, + }); + lastEndIndex = element.end; + }); + + // Handle any remaining text after the last element + const remainingText = text.substring(lastEndIndex); + const remainingParts = remainingText.split('\n'); + remainingParts.forEach((part, partIndex) => { + if (part) + elementsWithBreaks.push( + {part} + ); + if (partIndex < remainingParts.length - 1) + elementsWithBreaks.push(); + }); + + return elementsWithBreaks; + }, [text, mentions, LinkComponent, TextComponent, MentionComponent, BreakComponent]); + + return {processedText}; +} + +type addLinkElementProps = { + result: ReactNode[]; + element: TextElement; + index: number; +} & Pick; + +const addLinkElement = ({ + result, + element, + index, + + LinkComponent, + MentionComponent, +}: addLinkElementProps) => { + if (element.type === 'mention' && MentionComponent) { + result.push( + + ); + } else if (element.type === 'url' && LinkComponent) { + result.push(); + } else if (element.type === 'markdown-link' && element.url && LinkComponent) { + result.push(); + } +}; diff --git a/packages/shared/src/components/GalleryProccessedText/GalleryTextElementParser.ts b/packages/shared/src/components/GalleryProccessedText/GalleryTextElementParser.ts new file mode 100644 index 0000000000..46bb23d4fa --- /dev/null +++ b/packages/shared/src/components/GalleryProccessedText/GalleryTextElementParser.ts @@ -0,0 +1,134 @@ +import { graphql, readInlineData } from 'react-relay'; + +import { GalleryProcessedTextFragment$data } from '~/generated/GalleryProcessedTextFragment.graphql'; +import { + GalleryTextElementParserMentionsFragment$data, + GalleryTextElementParserMentionsFragment$key, +} from '~/generated/GalleryTextElementParserMentionsFragment.graphql'; + +import { MARKDOWN_LINK_REGEX, VALID_URL } from '../../utils/regex'; + +export type TextElement = { + type: 'mention' | 'url' | 'markdown-link'; + value: string; + start: number; + end: number; + url?: string; + mentionData?: GalleryTextElementParserMentionsFragment$data['entity']; +}; + +export function getMentionElements( + text: string, + mentionRefs: GalleryProcessedTextFragment$data +): TextElement[] { + function fetchMention(mentionRef: GalleryTextElementParserMentionsFragment$key) { + return readInlineData( + graphql` + fragment GalleryTextElementParserMentionsFragment on Mention @inline { + interval { + __typename + start + length + } + entity { + __typename + ... on GalleryUser { + __typename + username + } + ... on Community { + __typename + contractAddress { + __typename + address + chain + } + } + } + } + `, + mentionRef + ); + } + + const mentions: GalleryTextElementParserMentionsFragment$data[] = []; + + mentionRefs.forEach((mentionRef) => { + mentions.push(fetchMention(mentionRef)); + }); + + const elements: TextElement[] = []; + + mentions?.forEach((mention) => { + if (!mention?.entity || !mention?.interval) return; + + const { start, length } = mention.interval; + const mentionText = text.substring(start, start + length); + + elements.push({ + type: 'mention', + value: mentionText, + start: start, + end: start + length, + mentionData: mention.entity, + }); + }); + + return elements; +} + +export function getMarkdownLinkElements(text: string): TextElement[] { + const elements: TextElement[] = []; + + // Identify markdown-style links and add them to the elements array + const markdownLinkMatches = text.matchAll(MARKDOWN_LINK_REGEX); + for (const match of markdownLinkMatches) { + const [fullMatch, linkText, linkUrl] = match; + const startIndex = match.index ?? 0; + + elements.push({ + type: 'markdown-link', + // If there's no link text, then we use the link URL as the value + value: linkText ?? linkUrl ?? '', + start: startIndex, + end: startIndex + fullMatch.length, + url: linkUrl, + }); + } + + return elements; +} + +export function getUrlElements(text: string, existingElements: TextElement[] = []): TextElement[] { + const elements: TextElement[] = []; + + const URL_REGEX = new RegExp(VALID_URL, 'g'); // Make sure the URL regex has the 'g' flag + let urlMatch; + while ((urlMatch = URL_REGEX.exec(text)) !== null) { + const [match] = urlMatch; + const startIndex = urlMatch.index; + const endIndex = startIndex + match.length; + + // Before pushing a URL to the elements array, we check if it's within a markdown link. + // If it's not, then we push it. + if (!isWithinMarkdownLink(startIndex, endIndex, existingElements)) { + elements.push({ + type: 'url', + value: match, + start: startIndex, + end: endIndex, + }); + } + } + + return elements; +} + +function isWithinMarkdownLink(start: number, end: number, elements: TextElement[]) { + for (const element of elements) { + if (element.type === 'markdown-link' && start >= element.start && end <= element.end) { + return true; + } + } + return false; +} diff --git a/packages/shared/src/components/GalleryProccessedText/types.ts b/packages/shared/src/components/GalleryProccessedText/types.ts new file mode 100644 index 0000000000..06f60eb044 --- /dev/null +++ b/packages/shared/src/components/GalleryProccessedText/types.ts @@ -0,0 +1,19 @@ +import { ReactNode } from 'react'; + +import { GalleryTextElementParserMentionsFragment$data } from '~/generated/GalleryTextElementParserMentionsFragment.graphql'; + +type TextElement = React.ComponentType<{ children: ReactNode }>; +type MentionElement = React.ComponentType<{ + mention: string; + mentionData: GalleryTextElementParserMentionsFragment$data['entity']; +}>; +type LinkElement = React.ComponentType<{ url: string; value?: string }>; + +type BreakElement = React.ComponentType<{ key: string }>; + +export type SupportedProcessedTextElements = { + BreakComponent: BreakElement; + TextComponent: TextElement; + MentionComponent: MentionElement; + LinkComponent: LinkElement; +}; diff --git a/packages/shared/src/contexts/AnalyticsContext.tsx b/packages/shared/src/contexts/AnalyticsContext.tsx index 0a34fe08b6..7fed8b2566 100644 --- a/packages/shared/src/contexts/AnalyticsContext.tsx +++ b/packages/shared/src/contexts/AnalyticsContext.tsx @@ -12,6 +12,10 @@ import { fetchQuery, graphql } from 'relay-runtime'; import { AnalyticsContextQuery } from '~/generated/AnalyticsContextQuery.graphql'; +import { AnalyticsEventContextType, AnalyticsEventFlowType } from '../analytics/constants'; +import { noop } from '../utils/noop'; +import { removeNullishValues } from '../utils/removeNullishValues'; + type EventProps = Record; export type GalleryElementTrackingProps = { @@ -19,9 +23,17 @@ export type GalleryElementTrackingProps = { // this should be unique across the app. // e.g. `Feed Username Button` eventElementId: string | null; - // name of the action. this can be duplicated. + // a generalized name of the action. this can be duplicated + // across several elements, if several elements can trigger + // the same event. // e.g. `Follow User` eventName: string | null; + // a bucket, category, or general location for the event. + // e.g. `Authentication`, `Web Editor` + eventContext: AnalyticsEventContextType | null; + // an explicit user flow that the event falls into + // e.g. `Add Wallet Flow` or `Post Flow` + eventFlow?: AnalyticsEventFlowType | null; // custom metadata. // e.g. { variant: 'Worldwide' } properties?: EventProps; @@ -35,7 +47,7 @@ export const useTrack = () => { const track = useContext(AnalyticsContext); if (!track) { - return () => {}; + return noop; } return track; @@ -67,6 +79,8 @@ type Props = { const AnalyticsProvider = memo(({ children, identify, track, registerSuperProperties }: Props) => { const relayEnvironment = useRelayEnvironment(); + // @ts-expect-error: not tracking beta tester for now + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [isBetaTester, setIsBetaTester] = useState(false); useEffect(() => { @@ -89,16 +103,17 @@ const AnalyticsProvider = memo(({ children, identify, track, registerSuperProper if (user.roles?.includes('BETA_TESTER')) { setIsBetaTester(true); - registerSuperProperties({ isBetaTester: true }); + // not tracking beta tester for now + // registerSuperProperties({ isBetaTester: true }); } }); }, [identify, registerSuperProperties, relayEnvironment]); const handleTrack: HookTrackFunction = useCallback( (eventName, eventProps = {}) => { - track(eventName, { ...eventProps, isBetaTester }); + track(eventName, removeNullishValues(eventProps)); }, - [isBetaTester, track] + [track] ); return {children}; diff --git a/packages/shared/src/hooks/useCreateNonce.ts b/packages/shared/src/hooks/useCreateNonce.ts index 7a199a99e5..f8ac9aac8f 100644 --- a/packages/shared/src/hooks/useCreateNonce.ts +++ b/packages/shared/src/hooks/useCreateNonce.ts @@ -1,10 +1,10 @@ import { useCallback } from 'react'; import { graphql } from 'relay-runtime'; -import { Web3Error } from 'src/utils/Error'; import { Chain, useCreateNonceMutation } from '~/generated/useCreateNonceMutation.graphql'; import { usePromisifiedMutation } from '../relay/usePromisifiedMutation'; +import { Web3Error } from '../utils/Error'; /** * Retrieve a nonce for the client to sign given a wallet address. diff --git a/packages/shared/src/utils/extractRelevantMetadataFromToken.ts b/packages/shared/src/utils/extractRelevantMetadataFromToken.ts index 48a2934ede..3efa789874 100644 --- a/packages/shared/src/utils/extractRelevantMetadataFromToken.ts +++ b/packages/shared/src/utils/extractRelevantMetadataFromToken.ts @@ -6,7 +6,6 @@ import { isChainEvm } from './chains'; import { extractMirrorXyzUrl } from './extractMirrorXyzUrl'; import { DateFormatOption, getFormattedDate } from './getFormattedDate'; import { getOpenseaExternalUrlDangerously } from './getOpenseaExternalUrl'; -import { getProhibitionUrlDangerously } from './getProhibitionUrl'; import { getFxHashExternalUrlDangerously, getObjktExternalUrlDangerously, @@ -14,6 +13,7 @@ import { } from './getTezosExternalUrl'; import { hexToDec } from './hexToDec'; import processProjectUrl from './processProjectUrl'; +import { getProhibitionUrlDangerously } from './prohibition'; import { truncateAddress } from './wallet'; export function extractRelevantMetadataFromToken( diff --git a/packages/shared/src/utils/getProhibitionUrl.ts b/packages/shared/src/utils/getProhibitionUrl.ts deleted file mode 100644 index 57fd9966de..0000000000 --- a/packages/shared/src/utils/getProhibitionUrl.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { hexToDec } from './hexToDec'; - -const PROHIBITION_CONTRACT_ADDRESSES = new Set(['0x47a91457a3a1f700097199fd63c039c4784384ab']); - -/** - * WARNING: you will rarely want to use this function directly; - * prefer to use `extractRelevantMetadataFromToken.ts` - */ -export function getProhibitionUrlDangerously(contractAddress: string, tokenId: string) { - if (PROHIBITION_CONTRACT_ADDRESSES.has(contractAddress)) { - return `https://prohibition.art/token/${contractAddress}-${hexToDec(tokenId)}`; - } - return ''; -} diff --git a/packages/shared/src/utils/highlighter.ts b/packages/shared/src/utils/highlighter.ts new file mode 100644 index 0000000000..7e7a052ae1 --- /dev/null +++ b/packages/shared/src/utils/highlighter.ts @@ -0,0 +1,35 @@ +const MAX_DESCRIPTION_CHARACTER = 150; + +export function getHighlightedName(text: string, keyword: string) { + return text.replace(new RegExp(keyword, 'gi'), (match) => `**${match}**`); +} + +export function getHighlightedDescription(text: string, keyword: string) { + const regex = new RegExp(keyword, 'gi'); + + const unformattedDescription = sanitizeMarkdown(text ?? ''); + if (!keyword) { + return unformattedDescription.substring(0, MAX_DESCRIPTION_CHARACTER); + } + + const matchIndex = unformattedDescription.search(regex); + let truncatedDescription; + + const maxLength = MAX_DESCRIPTION_CHARACTER; + + if (matchIndex > -1 && matchIndex + keyword.length === unformattedDescription.length) { + const endIndex = Math.min(unformattedDescription.length, maxLength); + truncatedDescription = `...${unformattedDescription.substring(endIndex - maxLength, endIndex)}`; + } else { + truncatedDescription = unformattedDescription.substring(0, maxLength); + } + // highlight keyword + return truncatedDescription.replace(regex, (match) => `**${match}**`); +} + +function sanitizeMarkdown(text: string) { + return text + .replace(/\*\*/g, '') // bold + .replace(/\[([^[]*)\]\([^)]*\)/g, '$1') // link markdown tag from description + .replace(/\n/g, ' '); // break line +} diff --git a/packages/shared/src/utils/prohibition.test.ts b/packages/shared/src/utils/prohibition.test.ts new file mode 100644 index 0000000000..b29bf1fd03 --- /dev/null +++ b/packages/shared/src/utils/prohibition.test.ts @@ -0,0 +1,21 @@ +import { isKnownComputeIntensiveToken } from './prohibition'; + +describe('prohibition', () => { + test('isKnownComputeIntensiveToken', () => { + // prohibition + offending tokens + expect( + isKnownComputeIntensiveToken('0x47a91457a3a1f700097199fd63c039c4784384ab', '1E84805') // 32000005 + ).toBe(true); + expect( + isKnownComputeIntensiveToken('0x47a91457a3a1f700097199fd63c039c4784384ab', '56C8D23') // 91000099 + ).toBe(true); + + // non-prohibition contract + expect(isKnownComputeIntensiveToken('some_other_contract', '1E84805')).toBe(false); + + // prohibition + non-offending tokens + expect( + isKnownComputeIntensiveToken('0x47a91457a3a1f700097199fd63c039c4784384ab', 'F4240') // 1000000 + ).toBe(false); + }); +}); diff --git a/packages/shared/src/utils/prohibition.ts b/packages/shared/src/utils/prohibition.ts new file mode 100644 index 0000000000..5ff4a6443a --- /dev/null +++ b/packages/shared/src/utils/prohibition.ts @@ -0,0 +1,32 @@ +import { hexToDec } from './hexToDec'; + +const PROHIBITION_CONTRACT_ADDRESSES = new Set(['0x47a91457a3a1f700097199fd63c039c4784384ab']); + +// Projects that are known to crash browsers +const LIVE_RENDER_DISABLED_PROJECT_IDS = new Set([32, 91, 210]); + +/** + * WARNING: you will rarely want to use this function directly; + * prefer to use `extractRelevantMetadataFromToken.ts` + */ +export function getProhibitionUrlDangerously(contractAddress: string, tokenId: string) { + if (PROHIBITION_CONTRACT_ADDRESSES.has(contractAddress)) { + return `https://prohibition.art/token/${contractAddress}-${hexToDec(tokenId)}`; + } + return ''; +} + +function getProhibitionProjectId(tokenId: string) { + // same formula as art blocks + return Math.floor(Number(tokenId) / 1000000); +} + +export function isKnownComputeIntensiveToken(contractAddress: string, tokenId: string) { + if (PROHIBITION_CONTRACT_ADDRESSES.has(contractAddress)) { + const projectId = getProhibitionProjectId(hexToDec(tokenId)); + if (LIVE_RENDER_DISABLED_PROJECT_IDS.has(projectId)) { + return true; + } + } + return false; +} diff --git a/packages/shared/src/utils/regex.ts b/packages/shared/src/utils/regex.ts index 277e71d9ce..0a186dc581 100644 --- a/packages/shared/src/utils/regex.ts +++ b/packages/shared/src/utils/regex.ts @@ -22,3 +22,6 @@ export const HTTPS_URL = /^https?:\/\//i; // check if ethereum address export const ETH_ADDRESS = /^0x[a-fA-F0-9]{40}$/; + +// check markdown link +export const MARKDOWN_LINK_REGEX = /\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g; diff --git a/packages/shared/src/utils/removeNullishValues.test.ts b/packages/shared/src/utils/removeNullishValues.test.ts new file mode 100644 index 0000000000..047dc74088 --- /dev/null +++ b/packages/shared/src/utils/removeNullishValues.test.ts @@ -0,0 +1,56 @@ +import { removeNullishValues } from './removeNullishValues'; + +// thank you ChatGPT +describe('removeNullishValues', () => { + it('should remove properties with null values', () => { + const input = { + a: 'apple', + b: null, + }; + const output = removeNullishValues(input); + expect(output).toEqual({ a: 'apple' }); + }); + + it('should remove properties with undefined values', () => { + const input = { + a: 'apple', + b: undefined, + }; + const output = removeNullishValues(input); + expect(output).toEqual({ a: 'apple' }); + }); + + it('should keep properties with other falsy values', () => { + const input = { + a: 'apple', + b: 0, + c: false, + d: '', + }; + const output = removeNullishValues(input); + expect(output).toEqual({ + a: 'apple', + b: 0, + c: false, + d: '', + }); + }); + + it('should return an empty object if all properties are null or undefined', () => { + const input = { + a: null, + b: undefined, + }; + const output = removeNullishValues(input); + expect(output).toEqual({}); + }); + + it('should return the same object if there are no null or undefined properties', () => { + const input = { + a: 'apple', + b: 42, + }; + const output = removeNullishValues(input); + expect(output).toEqual(input); + }); +}); diff --git a/packages/shared/src/utils/removeNullishValues.ts b/packages/shared/src/utils/removeNullishValues.ts new file mode 100644 index 0000000000..361a7f0768 --- /dev/null +++ b/packages/shared/src/utils/removeNullishValues.ts @@ -0,0 +1,10 @@ +type Obj = Record; + +export function removeNullishValues(obj: Obj) { + return Object.keys(obj) + .filter((key) => obj[key] !== null && obj[key] !== undefined) + .reduce((newObj: Obj, key) => { + newObj[key] = obj[key]; + return newObj; + }, {}); +} diff --git a/packages/shared/src/utils/time.ts b/packages/shared/src/utils/time.ts index edd6c2cd76..aeea355513 100644 --- a/packages/shared/src/utils/time.ts +++ b/packages/shared/src/utils/time.ts @@ -36,3 +36,13 @@ export const getTimeSince = (time: string) => { export const getDaysSince = (time: string) => { return Math.floor((Date.now() - new Date(time).getTime()) / (1000 * 60 * 60 * 24)); }; + +export const getTimestamp = () => { + const now = new Date(); + + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const seconds = String(now.getSeconds()).padStart(2, '0'); + + return `${hours}:${minutes}:${seconds}`; +}; diff --git a/schema.graphql b/schema.graphql index 432c879e1e..845c47d433 100644 --- a/schema.graphql +++ b/schema.graphql @@ -2142,6 +2142,7 @@ enum UserExperienceType { MobileBetaUpsell UpsellMintMemento5 UpsellBanner + PostsBetaAnnouncement } type UserFollowedUsersFeedEventData implements FeedEventData {