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