This repository has no description
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}