alpha
Login
or
Join now
nandi.uk
/
semble
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
This repository has no description
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
feat: add to semble button
author
Pouria Delfanazari
date
8 months ago
(Nov 4, 2025, 11:31 AM -0800)
commit
6fcaf215
6fcaf215e01c3591950e3298274d4d1c4ee1127a
parent
fa1d3fe4
fa1d3fe4efe04881c34766255280ae8b1bca2b06
+91
-37
7 changed files
Expand all
Collapse all
Unified
Split
src
webapp
features
cards
components
addCardToModal
AddCardToModal.tsx
AddCardToModalContent.tsx
cardToBeAddedPreview
CardToBeAddedPreview.tsx
urlCardActions
UrlCardActions.tsx
lib
queries
useUrlMetadata.tsx
semble
components
SembleHeader
SembleHeader.tsx
sembleActions
SembleActions.tsx
+36
-19
src/webapp/features/cards/components/addCardToModal/AddCardToModal.tsx
View file
Reviewed
···
1
1
-
import type { UrlCard } from '@/api-client';
2
1
import { DEFAULT_OVERLAY_PROPS } from '@/styles/overlays';
3
2
import { Modal, Stack, Text } from '@mantine/core';
4
3
import { Suspense } from 'react';
5
4
import CollectionSelectorSkeleton from '@/features/collections/components/collectionSelector/Skeleton.CollectionSelector';
6
6
-
import AddCardToModalContent from './AddCardToModalContent'; // new file or inline
5
5
+
import AddCardToModalContent from './AddCardToModalContent';
7
6
8
7
interface Props {
9
8
isOpen: boolean;
10
9
onClose: () => void;
11
11
-
cardContent: UrlCard['cardContent'];
12
12
-
urlLibraryCount: number;
13
13
-
cardId: string;
10
10
+
url: string;
11
11
+
cardId?: string;
14
12
note?: string;
15
15
-
isInYourLibrary: boolean;
13
13
+
urlLibraryCount?: number;
14
14
+
isInYourLibrary?: boolean;
16
15
}
17
16
18
17
export default function AddCardToModal(props: Props) {
18
18
+
const {
19
19
+
isOpen,
20
20
+
onClose,
21
21
+
url,
22
22
+
cardId,
23
23
+
note,
24
24
+
urlLibraryCount,
25
25
+
isInYourLibrary,
26
26
+
} = props;
27
27
+
28
28
+
const count = urlLibraryCount ?? 0;
29
29
+
30
30
+
const subtitle = (() => {
31
31
+
if (count === 0) return 'Not saved by anyone yet';
32
32
+
33
33
+
if (isInYourLibrary) {
34
34
+
if (count === 1) return 'Saved by you';
35
35
+
return `Saved by you and ${count - 1} other${count - 1 > 1 ? 's' : ''}`;
36
36
+
} else {
37
37
+
if (count === 1) return 'Saved by 1 person';
38
38
+
return `Saved by ${count} people`;
39
39
+
}
40
40
+
})();
41
41
+
19
42
return (
20
43
<Modal
21
21
-
opened={props.isOpen}
22
22
-
onClose={props.onClose}
44
44
+
opened={isOpen}
45
45
+
onClose={onClose}
23
46
title={
24
47
<Stack gap={0}>
25
48
<Text fw={600}>Add or update card</Text>
26
49
<Text c="gray" fw={500}>
27
27
-
{props.isInYourLibrary
28
28
-
? props.urlLibraryCount === 1
29
29
-
? 'Saved by you'
30
30
-
: `Saved by you and ${props.urlLibraryCount - 1} other${props.urlLibraryCount - 1 > 1 ? 's' : ''}`
31
31
-
: props.urlLibraryCount === 1
32
32
-
? 'Saved by 1 person'
33
33
-
: `Saved by ${props.urlLibraryCount} people`}
50
50
+
{subtitle}
34
51
</Text>
35
52
</Stack>
36
53
}
···
40
57
>
41
58
<Suspense fallback={<CollectionSelectorSkeleton />}>
42
59
<AddCardToModalContent
43
43
-
onClose={props.onClose}
44
44
-
cardId={props.cardId}
45
45
-
cardContent={props.cardContent}
46
46
-
note={props.note}
60
60
+
onClose={onClose}
61
61
+
url={url}
62
62
+
cardId={cardId}
63
63
+
note={note}
47
64
/>
48
65
</Suspense>
49
66
</Modal>
+12
-6
src/webapp/features/cards/components/addCardToModal/AddCardToModalContent.tsx
View file
Reviewed
···
11
11
import useMyCollections from '@/features/collections/lib/queries/useMyCollections';
12
12
import useUpdateCardAssociations from '@/features/cards/lib/mutations/useUpdateCardAssociations';
13
13
import useAddCard from '@/features/cards/lib/mutations/useAddCard';
14
14
+
import useUrlMetadata from '../../lib/queries/useUrlMetadata';
14
15
15
16
interface SelectableCollectionItem {
16
17
id: string;
···
20
21
21
22
interface Props {
22
23
onClose: () => void;
23
23
-
cardContent: UrlCard['cardContent'];
24
24
-
cardId: string;
24
24
+
url: string;
25
25
+
cardId?: string;
25
26
note?: string;
26
27
}
27
28
28
29
export default function AddCardToModalContent(props: Props) {
29
29
-
const cardStatus = useGetCardFromMyLibrary({ url: props.cardContent.url });
30
30
-
const isMyCard = props.cardId === cardStatus.data.card?.id;
30
30
+
const {
31
31
+
data: { metadata },
32
32
+
} = useUrlMetadata({ url: props.url });
33
33
+
const cardStatus = useGetCardFromMyLibrary({ url: props.url });
34
34
+
const isMyCard = props?.cardId === cardStatus.data.card?.id;
31
35
const [note, setNote] = useState(isMyCard ? props.note : '');
32
36
const { data, error } = useMyCollections();
33
37
···
77
81
if (!cardStatus.data.card) {
78
82
addCard.mutate(
79
83
{
80
80
-
url: props.cardContent.url,
84
84
+
url: props.url,
81
85
note: trimmedNote,
82
86
collectionIds: selectedCollections.map((c) => c.id),
83
87
},
···
122
126
return (
123
127
<Stack justify="space-between">
124
128
<CardToBeAddedPreview
125
125
-
cardContent={props.cardContent}
129
129
+
url={props.url}
130
130
+
thumbnailUrl={metadata.imageUrl}
131
131
+
title={metadata.title}
126
132
note={isMyCard ? note : cardStatus.data.card?.note?.text}
127
133
onUpdateNote={setNote}
128
134
/>
+11
-9
src/webapp/features/cards/components/cardToBeAddedPreview/CardToBeAddedPreview.tsx
View file
Reviewed
···
16
16
import { getDomain } from '@/lib/utils/link';
17
17
18
18
interface Props {
19
19
-
cardContent: UrlCard['cardContent'];
19
19
+
url: string;
20
20
+
thumbnailUrl?: string;
21
21
+
title?: string;
20
22
note?: string;
21
23
onUpdateNote: Dispatch<SetStateAction<string | undefined>>;
22
24
}
···
24
26
export default function CardToBeAddedPreview(props: Props) {
25
27
const [noteMode, setNoteMode] = useState(false);
26
28
const [note, setNote] = useState(props.note);
27
27
-
const domain = getDomain(props.cardContent.url);
29
29
+
const domain = getDomain(props.url);
28
30
29
31
if (noteMode) {
30
32
return (
···
77
79
<Card withBorder component="article" p={'xs'} radius={'lg'}>
78
80
<Stack>
79
81
<Group gap={'sm'} justify="space-between">
80
80
-
{props.cardContent.thumbnailUrl && (
82
82
+
{props.thumbnailUrl && (
81
83
<AspectRatio ratio={1 / 1} flex={0.1}>
82
84
<Image
83
83
-
src={props.cardContent.thumbnailUrl}
84
84
-
alt={`${props.cardContent.url} social preview image`}
85
85
+
src={props.thumbnailUrl}
86
86
+
alt={`${props.url} social preview image`}
85
87
radius={'md'}
86
88
w={50}
87
89
h={50}
···
89
91
</AspectRatio>
90
92
)}
91
93
<Stack gap={0} flex={0.9}>
92
92
-
<Tooltip label={props.cardContent.url}>
94
94
+
<Tooltip label={props.url}>
93
95
<Anchor
94
96
component={Link}
95
95
-
href={props.cardContent.url}
97
97
+
href={props.url}
96
98
target="_blank"
97
99
c={'gray'}
98
100
lineClamp={1}
···
101
103
{domain}
102
104
</Anchor>
103
105
</Tooltip>
104
104
-
{props.cardContent.title && (
106
106
+
{props.title && (
105
107
<Text fw={500} lineClamp={1}>
106
106
-
{props.cardContent.title}
108
108
+
{props.title}
107
109
</Text>
108
110
)}
109
111
</Stack>
+1
-1
src/webapp/features/cards/components/urlCardActions/UrlCardActions.tsx
View file
Reviewed
···
118
118
<AddCardToModal
119
119
isOpen={showAddToModal}
120
120
onClose={() => setShowAddToModal(false)}
121
121
-
cardContent={props.cardContent}
121
121
+
url={props.cardContent.url}
122
122
cardId={props.id}
123
123
note={props.note?.text}
124
124
urlLibraryCount={props.urlLibraryCount}
+14
src/webapp/features/cards/lib/queries/useUrlMetadata.tsx
View file
Reviewed
···
1
1
+
import { useSuspenseQuery } from '@tanstack/react-query';
2
2
+
import { getUrlMetadata } from '../dal';
3
3
+
4
4
+
interface Props {
5
5
+
url: string;
6
6
+
}
7
7
+
8
8
+
export default function useUrlMetadata(props: Props) {
9
9
+
const metadata = useSuspenseQuery({
10
10
+
queryKey: [props.url],
11
11
+
queryFn: () => getUrlMetadata(props.url),
12
12
+
});
13
13
+
return metadata;
14
14
+
}
+3
-1
src/webapp/features/semble/components/SembleHeader/SembleHeader.tsx
View file
Reviewed
···
15
15
import { getDomain } from '@/lib/utils/link';
16
16
import UrlAddedBySummary from '../urlAddedBySummary/UrlAddedBySummary';
17
17
import SembleActions from '../sembleActions/SembleActions';
18
18
+
import { verifySessionOnServer } from '@/lib/auth/dal.server';
18
19
19
20
interface Props {
20
21
url: string;
···
22
23
23
24
export default async function SembleHeader(props: Props) {
24
25
const { metadata } = await getUrlMetadata(props.url);
26
26
+
const session = await verifySessionOnServer();
25
27
26
28
return (
27
29
<Stack gap={'xl'}>
···
74
76
/>
75
77
</Card>
76
78
)}
77
77
-
<SembleActions url={props.url} />
79
79
+
{session && <SembleActions url={props.url} />}
78
80
</Stack>
79
81
</GridCol>
80
82
</Grid>
+14
-1
src/webapp/features/semble/components/sembleActions/SembleActions.tsx
View file
Reviewed
···
1
1
'use client';
2
2
3
3
+
import AddCardToModal from '@/features/cards/components/addCardToModal/AddCardToModal';
3
4
import useGetCardFromMyLibrary from '@/features/cards/lib/queries/useGetCardFromMyLibrary';
4
5
import { Button, Group } from '@mantine/core';
5
5
-
import { Fragment } from 'react';
6
6
+
import { Fragment, useState } from 'react';
6
7
import { FiPlus } from 'react-icons/fi';
7
8
import { IoMdCheckmark } from 'react-icons/io';
8
9
···
13
14
export default function SembleActions(props: Props) {
14
15
const cardStatus = useGetCardFromMyLibrary({ url: props.url });
15
16
const isInYourLibrary = cardStatus.data.card?.urlInLibrary;
17
17
+
const [showAddToModal, setShowAddToModal] = useState(false);
16
18
17
19
if (cardStatus.error) {
18
20
return null;
···
27
29
leftSection={
28
30
isInYourLibrary ? <IoMdCheckmark size={18} /> : <FiPlus size={18} />
29
31
}
32
32
+
onClick={() => setShowAddToModal(true)}
30
33
>
31
34
{isInYourLibrary ? 'In library' : 'Add to library'}
32
35
</Button>
33
36
</Group>
37
37
+
38
38
+
<AddCardToModal
39
39
+
isOpen={showAddToModal}
40
40
+
onClose={() => setShowAddToModal(false)}
41
41
+
url={props.url}
42
42
+
cardId={cardStatus.data.card?.id}
43
43
+
note={cardStatus.data.card?.note?.text}
44
44
+
urlLibraryCount={cardStatus.data.card?.urlLibraryCount}
45
45
+
isInYourLibrary={cardStatus.data.card?.urlInLibrary}
46
46
+
/>
34
47
</Fragment>
35
48
);
36
49
}