This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / src / webapp / features / cards / components / urlCard / UrlCard.tsx
6.4 kB 190 lines
1'use client'; 2 3import type { UrlCard, Collection, User } from '@/api-client'; 4import { Anchor, Card, Group, Stack, Text } from '@mantine/core'; 5import { BsPinFill } from 'react-icons/bs'; 6import UrlCardActions from '../urlCardActions/UrlCardActions'; 7import { MouseEvent, Suspense } from 'react'; 8import UrlCardContent from '../urlCardContent/UrlCardContent'; 9import { useRouter } from 'next/navigation'; 10import { isCollectionPage, isProfilePage } from '@/lib/utils/link'; 11import styles from './UrlCard.module.css'; 12import { useUserSettings } from '@/features/settings/lib/queries/useUserSettings'; 13import UrlCardDebugView from '../UrlCardDebugView/UrlCardDebugView'; 14import { CardSaveAnalyticsContext } from '@/features/analytics/types'; 15import posthog from 'posthog-js'; 16import { shouldCaptureAnalytics } from '@/features/analytics/utils'; 17import UrlCardContentSkeleton from '../urlCardContent/Skeleton.UrlCardContent'; 18import { LinkAvatar } from '@/components/link/MantineLink'; 19 20interface Props { 21 id: string; 22 url: string; 23 uri?: string; 24 cardContent: UrlCard['cardContent']; 25 note?: UrlCard['note']; 26 currentCollection?: Collection; 27 urlLibraryCount: number; 28 urlIsInLibrary?: boolean; 29 urlConnectionCount: number; 30 urlIsConnected?: boolean; 31 authorHandle?: string; 32 cardAuthor?: User; 33 viaCardId?: string; 34 showAuthor?: boolean; 35 semblePageUrl?: string; 36 analyticsContext?: CardSaveAnalyticsContext; 37 isPinnedInCollection?: boolean; 38 onTogglePinInCollection?: () => void; 39} 40 41export default function UrlCard(props: Props) { 42 const router = useRouter(); 43 const { settings } = useUserSettings(); 44 45 const handleNavigateToSemblePage = (e: MouseEvent<HTMLElement>) => { 46 e.stopPropagation(); 47 48 let targetUrl: string; 49 const isNavigatingToSemble = 50 !isCollectionPage(props.url) && !isProfilePage(props.url); 51 52 if (isCollectionPage(props.url) || isProfilePage(props.url)) { 53 targetUrl = props.url; 54 } else { 55 // Build URL with viaCardId first, then id last (since id contains a URL that might have query params) 56 if (props.viaCardId) { 57 targetUrl = `/url?viaCardId=${props.id}&id=${props.cardContent.url}`; 58 } else { 59 targetUrl = `/url?id=${props.cardContent.url}`; 60 } 61 } 62 63 // Register super properties and capture click event when navigating to semble page 64 if ( 65 isNavigatingToSemble && 66 props.analyticsContext && 67 shouldCaptureAnalytics() 68 ) { 69 // Register super properties for later card_saved event 70 posthog.register({ 71 original_save_source: props.analyticsContext.saveSource, 72 original_active_filters: props.analyticsContext.activeFilters, 73 }); 74 75 // Capture card clicked event 76 posthog.capture('card_clicked', { 77 card_id: props.id, 78 url: props.cardContent.url, 79 save_source: props.analyticsContext.saveSource, 80 active_filters: props.analyticsContext.activeFilters, 81 via_card_id: props.viaCardId, 82 }); 83 } 84 85 // Open in new tab if Cmd+Click (Mac), Ctrl+Click (Windows/Linux), or middle click 86 if (e.metaKey || e.ctrlKey || e.button === 1) { 87 window.open(targetUrl, '_blank', 'noopener,noreferrer'); 88 return; 89 } 90 91 router.push(targetUrl); 92 }; 93 94 const handleAuxClick = (e: MouseEvent<HTMLElement>) => { 95 // Handle middle mouse button (button 1) 96 if (e.button === 1) { 97 handleNavigateToSemblePage(e); 98 } 99 }; 100 101 return ( 102 <Card 103 component="article" 104 radius={settings.cardView === 'list' ? 0 : 'lg'} 105 p={settings.cardView === 'list' ? 'xs' : 'sm'} 106 flex={1} 107 h={'100%'} 108 withBorder={settings.cardView !== 'list'} 109 className={styles.root} 110 onClick={handleNavigateToSemblePage} 111 onAuxClick={handleAuxClick} 112 > 113 <Stack justify="space-between" flex={1}> 114 {props.isPinnedInCollection && ( 115 <Group gap={5} c="dimmed"> 116 <BsPinFill size={12} /> 117 <Text fz="xs" fw={500}> 118 Pinned 119 </Text> 120 </Group> 121 )} 122 <Suspense fallback={<UrlCardContentSkeleton />}> 123 <UrlCardContent 124 url={props.url} 125 uri={props.uri} 126 cardContent={props.cardContent} 127 /> 128 </Suspense> 129 130 {settings.tinkerMode && ( 131 <UrlCardDebugView 132 cardContent={props.cardContent} 133 cardAuthor={props.cardAuthor} 134 /> 135 )} 136 137 <Stack> 138 {props.showAuthor && props.cardAuthor && ( 139 <Group gap={'7'}> 140 <Text fz={'xs'} c={'dimmed'} fw={500}> 141 By{' '} 142 </Text> 143 <Group gap={'5'}> 144 <LinkAvatar 145 href={`/profile/${props.cardAuthor?.handle}`} 146 src={props.cardAuthor?.avatarUrl?.replace( 147 'avatar', 148 'avatar_thumbnail', 149 )} 150 alt={`${props.cardAuthor?.handle}'s avatar`} 151 size={'xs'} 152 radius={'sm'} 153 /> 154 <Anchor 155 href={`/profile/${props.cardAuthor.handle}`} 156 fz={'xs'} 157 fw={600} 158 c={'bright'} 159 underline="never" 160 onClick={(e) => e.stopPropagation()} 161 > 162 {props.cardAuthor.name || `@${props.cardAuthor.handle}`} 163 </Anchor> 164 </Group> 165 </Group> 166 )} 167 168 <UrlCardActions 169 cardAuthor={props.cardAuthor} 170 cardContent={props.cardContent} 171 cardCount={props.urlLibraryCount} 172 id={props.id} 173 authorHandle={props.authorHandle} 174 note={props.note} 175 currentCollection={props.currentCollection} 176 urlLibraryCount={props.urlLibraryCount} 177 urlIsInLibrary={props.urlIsInLibrary ?? false} 178 urlConnectionCount={props.urlConnectionCount} 179 urlIsConnected={props.urlIsConnected} 180 viaCardId={props.viaCardId} 181 semblePageUrl={props.semblePageUrl} 182 analyticsContext={props.analyticsContext} 183 isPinnedInCollection={props.isPinnedInCollection} 184 onTogglePinInCollection={props.onTogglePinInCollection} 185 /> 186 </Stack> 187 </Stack> 188 </Card> 189 ); 190}