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 { 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}