+2
-2
src/webapp/.storybook/preview.tsx
+2
-2
src/webapp/.storybook/preview.tsx
···
1
1
import '@mantine/core/styles.css';
2
2
3
-
import React, { useEffect } from 'react';
4
-
import { MantineProvider, useMantineColorScheme } from '@mantine/core';
3
+
import React from 'react';
4
+
import { MantineProvider } from '@mantine/core';
5
5
import { theme } from '../styles/theme';
6
6
import { Hanken_Grotesk } from 'next/font/google';
7
7
+2
-2
src/webapp/app/(auth)/login/page.tsx
+2
-2
src/webapp/app/(auth)/login/page.tsx
···
71
71
<Popover withArrow shadow="sm">
72
72
<PopoverTarget>
73
73
<Button
74
-
variant="white"
74
+
variant="transparent"
75
75
size="md"
76
76
fw={500}
77
77
fs={'italic'}
78
-
c={'stone'}
78
+
c={'gray'}
79
79
rightSection={<IoMdHelpCircleOutline size={22} />}
80
80
>
81
81
How your Cosmik Network account works
+11
-2
src/webapp/app/(dashboard)/error.tsx
+11
-2
src/webapp/app/(dashboard)/error.tsx
···
13
13
} from '@mantine/core';
14
14
import SembleLogo from '@/assets/semble-logo.svg';
15
15
import BG from '@/assets/semble-bg.webp';
16
+
import DarkBG from '@/assets/semble-bg-dark.png';
16
17
import Link from 'next/link';
17
18
import { BiRightArrowAlt } from 'react-icons/bi';
19
+
import { useColorScheme } from '@mantine/hooks';
18
20
19
21
export default function Error() {
22
+
const colorScheme = useColorScheme();
23
+
20
24
return (
21
25
<BackgroundImage
22
-
src={BG.src}
26
+
src={colorScheme === 'dark' ? DarkBG.src : BG.src}
23
27
h={'100svh'}
24
28
pos={'fixed'}
25
29
top={0}
···
44
48
<Text fz={'h1'} fw={600} ta={'center'}>
45
49
A social knowledge network for researchers
46
50
</Text>
47
-
<Text fz={'h3'} fw={600} c={'#1F6144'} ta={'center'}>
51
+
<Text
52
+
fz={'h3'}
53
+
fw={600}
54
+
c={colorScheme === 'dark' ? '#1e4dd9' : '#1F6144'}
55
+
ta={'center'}
56
+
>
48
57
Follow your peers’ research trails. Surface and discover new
49
58
connections. Built on ATProto so you own your data.
50
59
</Text>
+1
-1
src/webapp/app/(dashboard)/profile/[handle]/(withHeader)/layout.tsx
+1
-1
src/webapp/app/(dashboard)/profile/[handle]/(withHeader)/layout.tsx
+5
src/webapp/app/(dashboard)/url/loading.tsx
+5
src/webapp/app/(dashboard)/url/loading.tsx
+1
-1
src/webapp/app/layout.tsx
+1
-1
src/webapp/app/layout.tsx
+23
-3
src/webapp/app/page.tsx
+23
-3
src/webapp/app/page.tsx
···
1
+
'use client';
2
+
1
3
import {
2
4
ActionIcon,
3
5
SimpleGrid,
···
18
20
import { BiRightArrowAlt } from 'react-icons/bi';
19
21
import { RiArrowRightUpLine } from 'react-icons/ri';
20
22
import BG from '@/assets/semble-bg.webp';
23
+
import DarkBG from '@/assets/semble-bg-dark.png';
21
24
import CosmikLogo from '@/assets/cosmik-logo-full.svg';
25
+
import CosmikLogoWhite from '@/assets/cosmik-logo-full-white.svg';
22
26
import CurateIcon from '@/assets/icons/curate-icon.svg';
23
27
import CommunityIcon from '@/assets/icons/community-icon.svg';
24
28
import DBIcon from '@/assets/icons/db-icon.svg';
···
26
30
import TangledIcon from '@/assets/icons/tangled-icon.svg';
27
31
import SembleLogo from '@/assets/semble-logo.svg';
28
32
import Link from 'next/link';
33
+
import { useColorScheme } from '@mantine/hooks';
29
34
30
35
export default function Home() {
36
+
const colorScheme = useColorScheme();
37
+
31
38
return (
32
-
<BackgroundImage src={BG.src} h={'100svh'}>
39
+
<BackgroundImage
40
+
src={colorScheme === 'dark' ? DarkBG.src : BG.src}
41
+
h={'100svh'}
42
+
>
33
43
<script async src="https://tally.so/widgets/embed.js" />
34
44
<Container size={'xl'} p={'md'} my={'auto'}>
35
45
<Group justify="space-between">
···
56
66
<Title order={1} fw={600} fz={'3rem'} ta={'center'}>
57
67
A social knowledge network for researchers
58
68
</Title>
59
-
<Title order={2} fw={600} fz={'xl'} c={'#1F6144'} ta={'center'}>
69
+
<Title
70
+
order={2}
71
+
fw={600}
72
+
fz={'xl'}
73
+
c={colorScheme === 'dark' ? '#1e4dd9' : '#1F6144'}
74
+
ta={'center'}
75
+
>
60
76
Follow your peers’ research trails. Surface and discover new
61
77
connections. Built on ATProto so you own your data.
62
78
</Title>
···
223
239
style={{ verticalAlign: 'middle' }}
224
240
>
225
241
<Image
226
-
src={CosmikLogo.src}
242
+
src={
243
+
colorScheme === 'dark'
244
+
? CosmikLogoWhite.src
245
+
: CosmikLogo.src
246
+
}
227
247
alt="Cosmik logo"
228
248
w={92}
229
249
h={28.4}
+10
src/webapp/assets/cosmik-logo-full-white.svg
+10
src/webapp/assets/cosmik-logo-full-white.svg
···
1
+
<svg width="144" height="46" viewBox="0 0 144 46" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+
<mask id="mask0_327_268" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="9" width="28" height="28">
3
+
<path d="M28 9.5H0V36.5H28V9.5Z" fill="white"/>
4
+
</mask>
5
+
<g mask="url(#mask0_327_268)">
6
+
<path d="M0 23.9995C0 30.9031 5.59644 36.4995 12.5 36.4995C18.7765 36.4995 23.9726 31.8735 24.8647 25.845C24.9411 25.3288 24.332 25.0129 23.8852 25.2825C22.6049 26.055 21.1043 26.4995 19.5 26.4995C14.8056 26.4995 11 22.6939 11 17.9995C11 15.9273 11.7415 14.0283 12.9737 12.5534C13.3084 12.1526 13.0889 11.5024 12.5667 11.4997L12.5 11.4995C5.59644 11.4995 0 17.096 0 23.9995Z" fill="white"/>
7
+
<path d="M28.0716 18C28.0716 13.3056 24.266 9.5 19.5716 9.5C17.9297 9.5 16.3966 9.96552 15.0969 10.7718C14.6532 11.047 14.8357 11.694 15.3446 11.8109C20.6468 13.0293 24.6392 17.7069 25.0383 23.1268C25.0767 23.6472 25.6879 23.9255 26.0275 23.5293C27.3018 22.0429 28.0716 20.1114 28.0716 18Z" fill="white"/>
8
+
</g>
9
+
<path d="M45.3063 32.3201C43.6885 32.3201 42.2752 31.9823 41.0663 31.3068C39.8574 30.6134 38.9152 29.6445 38.2396 28.4001C37.5818 27.1557 37.2529 25.689 37.2529 24.0001C37.2529 22.3112 37.5818 20.8445 38.2396 19.6001C38.8974 18.3557 39.8307 17.3957 41.0396 16.7201C42.2485 16.0268 43.6707 15.6801 45.3063 15.6801C47.2618 15.6801 48.8885 16.169 50.1863 17.1468C51.5018 18.1068 52.364 19.4934 52.7729 21.3068H49.7596C49.4574 20.2757 48.9329 19.5201 48.1863 19.0401C47.4574 18.5423 46.4974 18.2934 45.3063 18.2934C43.724 18.2934 42.4796 18.7912 41.5729 19.7868C40.684 20.7823 40.2396 22.1868 40.2396 24.0001C40.2396 25.7957 40.684 27.2001 41.5729 28.2134C42.4796 29.209 43.724 29.7068 45.3063 29.7068C47.7774 29.7068 49.2974 28.6134 49.8663 26.4268H52.8796C52.4885 28.329 51.6352 29.7868 50.3196 30.8001C49.004 31.8134 47.3329 32.3201 45.3063 32.3201ZM62.81 32.3201C61.1567 32.3201 59.7078 31.9823 58.4633 31.3068C57.2367 30.6134 56.2856 29.6445 55.61 28.4001C54.9345 27.1557 54.5967 25.689 54.5967 24.0001C54.5967 22.3112 54.9345 20.8445 55.61 19.6001C56.2856 18.3557 57.2367 17.3957 58.4633 16.7201C59.7078 16.0268 61.1567 15.6801 62.81 15.6801C64.5167 15.6801 65.9745 16.0179 67.1834 16.6934C68.41 17.369 69.3522 18.329 70.01 19.5734C70.6856 20.8179 71.0234 22.2934 71.0234 24.0001C71.0234 25.689 70.6856 27.1645 70.01 28.4268C69.3522 29.6712 68.41 30.6312 67.1834 31.3068C65.9567 31.9823 64.4989 32.3201 62.81 32.3201ZM62.81 29.7068C64.4634 29.7068 65.7434 29.209 66.65 28.2134C67.5745 27.2001 68.0367 25.7957 68.0367 24.0001C68.0367 22.1868 67.5745 20.7823 66.65 19.7868C65.7434 18.7912 64.4634 18.2934 62.81 18.2934C61.1745 18.2934 59.8945 18.8001 58.97 19.8134C58.0456 20.809 57.5834 22.2045 57.5834 24.0001C57.5834 25.7957 58.0456 27.2001 58.97 28.2134C59.8945 29.209 61.1745 29.7068 62.81 29.7068ZM80.6542 32.3201C78.432 32.3201 76.6809 31.8579 75.4009 30.9334C74.1209 30.009 73.4186 28.7201 73.2942 27.0668H76.2542C76.3786 28.0445 76.8053 28.7645 77.5342 29.2268C78.2631 29.689 79.3386 29.9201 80.7609 29.9201C83.2497 29.9201 84.4942 29.1379 84.4942 27.5734C84.4942 26.9334 84.2986 26.4445 83.9075 26.1068C83.5342 25.7512 82.8942 25.4934 81.9875 25.3334L78.4942 24.6934C75.5253 24.1423 74.0408 22.729 74.0408 20.4534C74.0408 18.9779 74.6009 17.8134 75.7209 16.9601C76.8408 16.1068 78.3697 15.6801 80.3075 15.6801C82.3164 15.6801 83.9075 16.1157 85.0809 16.9868C86.272 17.8579 86.9475 19.0845 87.1075 20.6668H84.2009C84.0231 19.7779 83.6053 19.1201 82.9475 18.6934C82.3075 18.2668 81.4009 18.0534 80.2275 18.0534C79.1431 18.0534 78.2986 18.2401 77.6942 18.6134C77.1075 18.9868 76.8142 19.529 76.8142 20.2401C76.8142 20.7912 77.0008 21.2268 77.3742 21.5468C77.7653 21.849 78.3875 22.0801 79.2409 22.2401L82.7342 22.9068C84.2986 23.1912 85.4453 23.6979 86.1742 24.4268C86.9031 25.1557 87.2675 26.1334 87.2675 27.3601C87.2675 28.9245 86.6986 30.1423 85.5609 31.0134C84.4231 31.8845 82.7875 32.3201 80.6542 32.3201ZM91.0717 32.0001V19.0934H89.525V16.0001H92.725V18.4534H94.005C94.4672 17.5645 95.1339 16.889 96.005 16.4268C96.8939 15.9468 97.9606 15.7068 99.205 15.7068C100.468 15.7068 101.534 15.9557 102.405 16.4534C103.294 16.9512 104.005 17.7157 104.539 18.7468H104.619C105.739 16.7201 107.579 15.7068 110.139 15.7068C112.112 15.7068 113.65 16.2757 114.752 17.4134C115.854 18.5512 116.405 20.1157 116.405 22.1068V32.0001H113.419V22.5334C113.419 19.7245 112.05 18.3201 109.312 18.3201C106.592 18.3201 105.232 19.7245 105.232 22.5334V32.0001H102.245V22.5334C102.245 19.7245 100.877 18.3201 98.1384 18.3201C96.805 18.3201 95.7917 18.6845 95.0984 19.4134C94.405 20.1245 94.0584 21.1645 94.0584 22.5334V32.0001H91.0717ZM120.789 32.0001V16.0001H123.775V32.0001H120.789ZM122.282 13.8134C121.784 13.8134 121.358 13.6357 121.002 13.2801C120.647 12.9245 120.469 12.4979 120.469 12.0001C120.469 11.5023 120.647 11.0757 121.002 10.7201C121.358 10.3645 121.784 10.1868 122.282 10.1868C122.78 10.1868 123.207 10.3645 123.562 10.7201C123.918 11.0757 124.095 11.5023 124.095 12.0001C124.095 12.4979 123.918 12.9245 123.562 13.2801C123.207 13.6357 122.78 13.8134 122.282 13.8134ZM128.254 32.0001V9.6001H131.24V23.2001H131.347L139.054 16.0001H142.707L135.374 22.6134L143.32 32.0001H139.694L133.347 24.4534L131.24 26.1601V32.0001H128.254Z" fill="white"/>
10
+
</svg>
src/webapp/assets/semble-bg-dark.png
src/webapp/assets/semble-bg-dark.png
This is a binary file and will not be displayed.
src/webapp/assets/semble-bg-dark.webp
src/webapp/assets/semble-bg-dark.webp
This is a binary file and will not be displayed.
src/webapp/assets/semble-header-bg-dark.webp
src/webapp/assets/semble-header-bg-dark.webp
This is a binary file and will not be displayed.
-1
src/webapp/components/AddToCollectionModal.tsx
-1
src/webapp/components/AddToCollectionModal.tsx
+2
-2
src/webapp/features/cards/components/addCardDrawer/AddCardDrawer.tsx
+2
-2
src/webapp/features/cards/components/addCardDrawer/AddCardDrawer.tsx
···
89
89
New Card
90
90
</Drawer.Title>
91
91
</Drawer.Header>
92
-
<Container size={'sm'}>
92
+
<Container size={'sm'} p={0}>
93
93
<form onSubmit={handleAddCard}>
94
94
<Stack gap={'xl'}>
95
95
<Stack>
···
152
152
Add to collections
153
153
</Drawer.Title>
154
154
</Drawer.Header>
155
-
<Container size={'xs'}>
155
+
<Container size={'xs'} p={0}>
156
156
<Suspense fallback={<CollectionSelectorSkeleton />}>
157
157
<CollectionSelector
158
158
isOpen={collectionSelectorOpened}
+39
-17
src/webapp/features/cards/components/addCardToModal/AddCardToModal.tsx
+39
-17
src/webapp/features/cards/components/addCardToModal/AddCardToModal.tsx
···
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
-
import AddCardToModalContent from './AddCardToModalContent'; // new file or inline
5
+
import AddCardToModalContent from './AddCardToModalContent';
7
6
8
7
interface Props {
9
8
isOpen: boolean;
10
9
onClose: () => void;
11
-
cardContent: UrlCard['cardContent'];
12
-
urlLibraryCount: number;
13
-
cardId: string;
10
+
url: string;
11
+
cardId?: string;
14
12
note?: string;
15
-
isInYourLibrary: boolean;
13
+
urlLibraryCount?: number;
14
+
isInYourLibrary?: boolean;
16
15
}
17
16
18
17
export default function AddCardToModal(props: Props) {
18
+
const {
19
+
isOpen,
20
+
onClose,
21
+
url,
22
+
cardId,
23
+
note,
24
+
urlLibraryCount,
25
+
isInYourLibrary,
26
+
} = props;
27
+
28
+
const count = urlLibraryCount ?? 0;
29
+
30
+
const subtitle = (() => {
31
+
if (count === 0) return 'Not saved by anyone yet';
32
+
33
+
if (isInYourLibrary) {
34
+
if (count === 1) return 'Saved by you';
35
+
return `Saved by you and ${count - 1} other${count - 1 > 1 ? 's' : ''}`;
36
+
} else {
37
+
if (count === 1) return 'Saved by 1 person';
38
+
return `Saved by ${count} people`;
39
+
}
40
+
})();
41
+
19
42
return (
20
43
<Modal
21
-
opened={props.isOpen}
22
-
onClose={props.onClose}
44
+
opened={isOpen}
45
+
onClose={onClose}
23
46
title={
24
47
<Stack gap={0}>
25
-
<Text fw={600}>Add or update card</Text>
48
+
<Text fw={600}>Add or update {props.cardId ? 'card' : 'link'}</Text>
26
49
<Text c="gray" fw={500}>
27
-
{props.isInYourLibrary
28
-
? props.urlLibraryCount === 1
29
-
? 'Saved by you'
30
-
: `Saved by you and ${props.urlLibraryCount - 1} other${props.urlLibraryCount - 1 > 1 ? 's' : ''}`
31
-
: props.urlLibraryCount === 1
32
-
? 'Saved by 1 person'
33
-
: `Saved by ${props.urlLibraryCount} people`}
50
+
{subtitle}
34
51
</Text>
35
52
</Stack>
36
53
}
···
39
56
onClick={(e) => e.stopPropagation()}
40
57
>
41
58
<Suspense fallback={<CollectionSelectorSkeleton />}>
42
-
<AddCardToModalContent {...props} />
59
+
<AddCardToModalContent
60
+
onClose={onClose}
61
+
url={url}
62
+
cardId={cardId}
63
+
note={note}
64
+
/>
43
65
</Suspense>
44
66
</Modal>
45
67
);
+14
-8
src/webapp/features/cards/components/addCardToModal/AddCardToModalContent.tsx
+14
-8
src/webapp/features/cards/components/addCardToModal/AddCardToModalContent.tsx
···
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
+
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
-
cardContent: UrlCard['cardContent'];
24
-
urlLibraryCount: number;
25
-
cardId: string;
24
+
url: string;
25
+
cardId?: string;
26
26
note?: string;
27
-
isInYourLibrary: boolean;
28
27
}
29
28
30
29
export default function AddCardToModalContent(props: Props) {
31
-
const cardStatus = useGetCardFromMyLibrary({ url: props.cardContent.url });
32
-
const isMyCard = props.cardId === cardStatus.data.card?.id;
30
+
const {
31
+
data: { metadata },
32
+
} = useUrlMetadata({ url: props.url });
33
+
const cardStatus = useGetCardFromMyLibrary({ url: props.url });
34
+
const isMyCard = props?.cardId === cardStatus.data.card?.id;
33
35
const [note, setNote] = useState(isMyCard ? props.note : '');
34
36
const { data, error } = useMyCollections();
35
37
···
79
81
if (!cardStatus.data.card) {
80
82
addCard.mutate(
81
83
{
82
-
url: props.cardContent.url,
84
+
url: props.url,
83
85
note: trimmedNote,
84
86
collectionIds: selectedCollections.map((c) => c.id),
85
87
},
···
124
126
return (
125
127
<Stack justify="space-between">
126
128
<CardToBeAddedPreview
127
-
cardContent={props.cardContent}
129
+
url={props.url}
130
+
thumbnailUrl={metadata.imageUrl}
131
+
title={metadata.title}
128
132
note={isMyCard ? note : cardStatus.data.card?.note?.text}
133
+
noteId={cardStatus.data.card?.note?.id}
129
134
onUpdateNote={setNote}
135
+
onClose={props.onClose}
130
136
/>
131
137
132
138
<CollectionSelector
+99
-37
src/webapp/features/cards/components/cardToBeAddedPreview/CardToBeAddedPreview.tsx
+99
-37
src/webapp/features/cards/components/cardToBeAddedPreview/CardToBeAddedPreview.tsx
···
12
12
} from '@mantine/core';
13
13
import Link from 'next/link';
14
14
import { Dispatch, SetStateAction, useState } from 'react';
15
-
import { UrlCard } from '@/api-client';
16
15
import { getDomain } from '@/lib/utils/link';
16
+
import useRemoveCardFromLibrary from '../../lib/mutations/useRemoveCardFromLibrary';
17
+
import { notifications } from '@mantine/notifications';
17
18
18
19
interface Props {
19
-
cardContent: UrlCard['cardContent'];
20
+
url: string;
21
+
thumbnailUrl?: string;
22
+
title?: string;
20
23
note?: string;
24
+
noteId?: string;
21
25
onUpdateNote: Dispatch<SetStateAction<string | undefined>>;
26
+
onClose: () => void;
22
27
}
23
28
24
29
export default function CardToBeAddedPreview(props: Props) {
30
+
const [showDeleteWarning, setShowDeleteWarning] = useState(false);
25
31
const [noteMode, setNoteMode] = useState(false);
26
32
const [note, setNote] = useState(props.note);
27
-
const domain = getDomain(props.cardContent.url);
33
+
const domain = getDomain(props.url);
34
+
35
+
const removeNote = useRemoveCardFromLibrary();
36
+
37
+
const handleDeleteNote = () => {
38
+
if (!props.noteId) return;
39
+
40
+
removeNote.mutate(props.noteId, {
41
+
onError: () => {
42
+
notifications.show({
43
+
message: 'Could not delete note.',
44
+
position: 'top-center',
45
+
});
46
+
},
47
+
onSettled: () => {
48
+
props.onClose();
49
+
},
50
+
});
51
+
};
28
52
29
53
if (noteMode) {
30
54
return (
···
74
98
}
75
99
76
100
return (
77
-
<Card withBorder component="article" p={'xs'} radius={'lg'}>
78
-
<Stack>
79
-
<Group gap={'sm'} justify="space-between">
80
-
{props.cardContent.thumbnailUrl && (
81
-
<AspectRatio ratio={1 / 1} flex={0.1}>
82
-
<Image
83
-
src={props.cardContent.thumbnailUrl}
84
-
alt={`${props.cardContent.url} social preview image`}
85
-
radius={'md'}
86
-
w={50}
87
-
h={50}
88
-
/>
89
-
</AspectRatio>
90
-
)}
91
-
<Stack gap={0} flex={0.9}>
92
-
<Tooltip label={props.cardContent.url}>
93
-
<Anchor
94
-
component={Link}
95
-
href={props.cardContent.url}
96
-
target="_blank"
97
-
c={'gray'}
98
-
lineClamp={1}
99
-
onClick={(e) => e.stopPropagation()}
100
-
>
101
-
{domain}
102
-
</Anchor>
103
-
</Tooltip>
104
-
{props.cardContent.title && (
105
-
<Text fw={500} lineClamp={1}>
106
-
{props.cardContent.title}
107
-
</Text>
101
+
<Stack gap={'xs'}>
102
+
<Card withBorder component="article" p={'xs'} radius={'lg'}>
103
+
<Stack>
104
+
<Group gap={'sm'} justify="space-between">
105
+
{props.thumbnailUrl && (
106
+
<AspectRatio ratio={1 / 1} flex={0.1}>
107
+
<Image
108
+
src={props.thumbnailUrl}
109
+
alt={`${props.url} social preview image`}
110
+
radius={'md'}
111
+
w={50}
112
+
h={50}
113
+
/>
114
+
</AspectRatio>
108
115
)}
109
-
</Stack>
116
+
<Stack gap={0} flex={0.9}>
117
+
<Tooltip label={props.url}>
118
+
<Anchor
119
+
component={Link}
120
+
href={props.url}
121
+
target="_blank"
122
+
c={'gray'}
123
+
lineClamp={1}
124
+
onClick={(e) => e.stopPropagation()}
125
+
>
126
+
{domain}
127
+
</Anchor>
128
+
</Tooltip>
129
+
{props.title && (
130
+
<Text fw={500} lineClamp={1}>
131
+
{props.title}
132
+
</Text>
133
+
)}
134
+
</Stack>
135
+
</Group>
136
+
</Stack>
137
+
</Card>
138
+
{showDeleteWarning ? (
139
+
<Group justify="space-between" gap={'xs'}>
140
+
<Text>Delete note?</Text>
141
+
<Group gap={'xs'}>
142
+
<Button
143
+
color="red"
144
+
onClick={handleDeleteNote}
145
+
loading={removeNote.isPending}
146
+
>
147
+
Delete
148
+
</Button>
149
+
<Button
150
+
variant="light"
151
+
color="gray"
152
+
onClick={() => setShowDeleteWarning(false)}
153
+
>
154
+
Cancel
155
+
</Button>
156
+
</Group>
157
+
</Group>
158
+
) : (
159
+
<Group gap={'xs'}>
110
160
<Button
111
161
variant="light"
112
162
color="gray"
···
117
167
>
118
168
{note ? 'Edit note' : 'Add note'}
119
169
</Button>
170
+
{props.noteId && (
171
+
<Button
172
+
variant="light"
173
+
color="red"
174
+
onClick={(e) => {
175
+
e.stopPropagation();
176
+
setShowDeleteWarning(true);
177
+
}}
178
+
>
179
+
Delete note
180
+
</Button>
181
+
)}
120
182
</Group>
121
-
</Stack>
122
-
</Card>
183
+
)}
184
+
</Stack>
123
185
);
124
186
}
+1
-2
src/webapp/features/cards/components/urlCardActions/UrlCardActions.tsx
+1
-2
src/webapp/features/cards/components/urlCardActions/UrlCardActions.tsx
···
32
32
const isAuthor = props.authorHandle
33
33
? user?.handle === props.authorHandle
34
34
: true;
35
-
const [showEditNoteModal, setShowEditNoteModal] = useState(false);
36
35
const [showNoteModal, setShowNoteModal] = useState(false);
37
36
const [showRemoveFromCollectionModal, setShowRemoveFromCollectionModal] =
38
37
useState(false);
···
119
118
<AddCardToModal
120
119
isOpen={showAddToModal}
121
120
onClose={() => setShowAddToModal(false)}
122
-
cardContent={props.cardContent}
121
+
url={props.cardContent.url}
123
122
cardId={props.id}
124
123
note={props.note?.text}
125
124
urlLibraryCount={props.urlLibraryCount}
+6
src/webapp/features/cards/lib/mutations/useAddCard.tsx
+6
src/webapp/features/cards/lib/mutations/useAddCard.tsx
···
2
2
import { addUrlToLibrary } from '../dal';
3
3
import { cardKeys } from '../cardKeys';
4
4
import { collectionKeys } from '@/features/collections/lib/collectionKeys';
5
+
import { feedKeys } from '@/features/feeds/lib/feedKeys';
5
6
6
7
export default function useAddCard() {
7
8
const queryClient = useQueryClient();
···
24
25
onSuccess: (_data, variables) => {
25
26
queryClient.invalidateQueries({ queryKey: cardKeys.mine() });
26
27
queryClient.invalidateQueries({ queryKey: cardKeys.all() });
28
+
queryClient.invalidateQueries({ queryKey: feedKeys.all() });
27
29
queryClient.invalidateQueries({ queryKey: collectionKeys.mine() });
28
30
queryClient.invalidateQueries({ queryKey: collectionKeys.infinite() });
31
+
queryClient.invalidateQueries({ queryKey: collectionKeys.all() });
29
32
queryClient.invalidateQueries({
30
33
queryKey: collectionKeys.bySembleUrl(variables.url),
31
34
});
···
34
37
variables.collectionIds?.forEach((id) => {
35
38
queryClient.invalidateQueries({
36
39
queryKey: collectionKeys.collection(id),
40
+
});
41
+
queryClient.invalidateQueries({
42
+
queryKey: collectionKeys.infinite(id),
37
43
});
38
44
});
39
45
},
+4
src/webapp/features/cards/lib/mutations/useRemoveCardFromCollections.tsx
+4
src/webapp/features/cards/lib/mutations/useRemoveCardFromCollections.tsx
···
19
19
onSuccess: (_data, variables) => {
20
20
queryClient.invalidateQueries({ queryKey: collectionKeys.infinite() });
21
21
queryClient.invalidateQueries({ queryKey: collectionKeys.mine() });
22
+
queryClient.invalidateQueries({ queryKey: collectionKeys.all() });
22
23
23
24
variables.collectionIds.forEach((id) => {
24
25
queryClient.invalidateQueries({
25
26
queryKey: collectionKeys.collection(id),
27
+
});
28
+
queryClient.invalidateQueries({
29
+
queryKey: collectionKeys.infinite(id),
26
30
});
27
31
});
28
32
},
+4
src/webapp/features/cards/lib/mutations/useRemoveCardFromLibrary.tsx
+4
src/webapp/features/cards/lib/mutations/useRemoveCardFromLibrary.tsx
···
2
2
import { removeCardFromLibrary } from '../dal';
3
3
import { cardKeys } from '../cardKeys';
4
4
import { collectionKeys } from '@/features/collections/lib/collectionKeys';
5
+
import { noteKeys } from '@/features/notes/lib/noteKeys';
6
+
import { feedKeys } from '@/features/feeds/lib/feedKeys';
5
7
6
8
export default function useRemoveCardFromLibrary() {
7
9
const queryClient = useQueryClient();
···
13
15
14
16
onSuccess: () => {
15
17
queryClient.invalidateQueries({ queryKey: cardKeys.all() });
18
+
queryClient.invalidateQueries({ queryKey: noteKeys.all() });
19
+
queryClient.invalidateQueries({ queryKey: feedKeys.all() });
16
20
queryClient.invalidateQueries({ queryKey: collectionKeys.all() });
17
21
},
18
22
});
+3
src/webapp/features/cards/lib/mutations/useUpdateCardAssociations.tsx
+3
src/webapp/features/cards/lib/mutations/useUpdateCardAssociations.tsx
···
4
4
import { collectionKeys } from '@/features/collections/lib/collectionKeys';
5
5
import { noteKeys } from '@/features/notes/lib/noteKeys';
6
6
import { sembleKeys } from '@/features/semble/lib/sembleKeys';
7
+
import { feedKeys } from '@/features/feeds/lib/feedKeys';
7
8
8
9
export default function useUpdateCardAssociations() {
9
10
const client = createSembleClient();
···
28
29
onSuccess: (_data, variables) => {
29
30
queryClient.invalidateQueries({ queryKey: cardKeys.all() });
30
31
queryClient.invalidateQueries({ queryKey: noteKeys.all() });
32
+
queryClient.invalidateQueries({ queryKey: feedKeys.all() });
31
33
queryClient.invalidateQueries({ queryKey: sembleKeys.all() });
32
34
queryClient.invalidateQueries({ queryKey: collectionKeys.mine() });
33
35
queryClient.invalidateQueries({ queryKey: collectionKeys.infinite() });
36
+
queryClient.invalidateQueries({ queryKey: collectionKeys.all() });
34
37
35
38
// invalidate each collection query individually
36
39
variables.addToCollectionIds?.forEach((id) => {
+14
src/webapp/features/cards/lib/queries/useUrlMetadata.tsx
+14
src/webapp/features/cards/lib/queries/useUrlMetadata.tsx
···
1
+
import { useSuspenseQuery } from '@tanstack/react-query';
2
+
import { getUrlMetadata } from '../dal';
3
+
4
+
interface Props {
5
+
url: string;
6
+
}
7
+
8
+
export default function useUrlMetadata(props: Props) {
9
+
const metadata = useSuspenseQuery({
10
+
queryKey: [props.url],
11
+
queryFn: () => getUrlMetadata(props.url),
12
+
});
13
+
return metadata;
14
+
}
+3
src/webapp/features/collections/components/collectionCard/CollectionCard.module.css
+3
src/webapp/features/collections/components/collectionCard/CollectionCard.module.css
+2
-1
src/webapp/features/collections/components/collectionCard/CollectionCard.tsx
+2
-1
src/webapp/features/collections/components/collectionCard/CollectionCard.tsx
···
4
4
import { getRecordKey } from '@/lib/utils/atproto';
5
5
import { getRelativeTime } from '@/lib/utils/time';
6
6
import { Avatar, Card, Group, Stack, Text } from '@mantine/core';
7
+
import styles from './CollectionCard.module.css';
7
8
import { useRouter } from 'next/navigation';
8
9
9
10
interface Props {
···
29
30
}
30
31
radius={'lg'}
31
32
p={'sm'}
32
-
style={{ cursor: 'pointer' }}
33
+
className={styles.root}
33
34
>
34
35
<Stack justify="space-between" h={'100%'}>
35
36
<Stack gap={0}>
+3
-1
src/webapp/features/collections/components/collectionSelector/CollectionSelector.tsx
+3
-1
src/webapp/features/collections/components/collectionSelector/CollectionSelector.tsx
···
11
11
Button,
12
12
Group,
13
13
Divider,
14
+
FocusTrap,
14
15
} from '@mantine/core';
15
16
import { Fragment, useState } from 'react';
16
17
import { useDebouncedValue } from '@mantine/hooks';
···
73
74
74
75
return (
75
76
<Fragment>
77
+
<FocusTrap.InitialFocus />
76
78
<Stack gap="xl">
77
79
<Stack>
78
80
<TextInput
···
92
94
}
93
95
/>
94
96
95
-
<ScrollArea.Autosize mah={340} type="auto">
97
+
<ScrollArea.Autosize mah={215} type="auto">
96
98
<Stack gap="xs">
97
99
{search ? (
98
100
<>
+1
-1
src/webapp/features/collections/components/createCollectionDrawer/CreateCollectionDrawer.tsx
+1
-1
src/webapp/features/collections/components/createCollectionDrawer/CreateCollectionDrawer.tsx
+1
-1
src/webapp/features/collections/components/editCollectionDrawer/EditCollectionDrawer.tsx
+1
-1
src/webapp/features/collections/components/editCollectionDrawer/EditCollectionDrawer.tsx
+1
-1
src/webapp/features/composer/components/composerDrawer/ComposerDrawer.tsx
+1
-1
src/webapp/features/composer/components/composerDrawer/ComposerDrawer.tsx
···
7
7
8
8
export default function ComposerDrawer() {
9
9
const { mobileOpened, desktopOpened } = useNavbarContext();
10
-
const isDesktop = useMediaQuery('(min-width: 36em)'); // "sm" breakpoint
10
+
const isDesktop = useMediaQuery('(min-width: 36em)', false); // "sm" breakpoint
11
11
const isNavOpen = isDesktop ? desktopOpened : mobileOpened;
12
12
const shouldShowFab = !isNavOpen;
13
13
const [opened, setOpened] = useState(false);
+55
-7
src/webapp/features/feeds/components/feedActivityStatus/FeedActivityStatus.tsx
+55
-7
src/webapp/features/feeds/components/feedActivityStatus/FeedActivityStatus.tsx
···
1
-
import { Anchor, Avatar, Group, Paper, Stack, Text } from '@mantine/core';
1
+
'use client';
2
+
3
+
import {
4
+
Anchor,
5
+
Avatar,
6
+
Card,
7
+
Group,
8
+
Menu,
9
+
ScrollArea,
10
+
Stack,
11
+
Text,
12
+
} from '@mantine/core';
2
13
import { FeedItem, Collection } from '@/api-client';
3
14
import { Fragment } from 'react';
4
15
import Link from 'next/link';
5
16
import { getRelativeTime } from '@/lib/utils/time';
6
17
import { getRecordKey } from '@/lib/utils/atproto';
7
18
import { sanitizeText } from '@/lib/utils/text';
19
+
import { useColorScheme } from '@mantine/hooks';
20
+
import { BiCollection } from 'react-icons/bi';
8
21
9
22
interface Props {
10
23
user: FeedItem['user'];
···
13
26
}
14
27
15
28
export default function FeedActivityStatus(props: Props) {
29
+
const colorScheme = useColorScheme();
16
30
const MAX_DISPLAYED = 2;
17
31
const time = getRelativeTime(props.createdAt.toString());
18
32
const relativeCreatedDate = time === 'just now' ? `Now` : `${time} ago`;
···
20
34
const renderActivityText = () => {
21
35
const collections = props.collections ?? [];
22
36
const displayedCollections = collections.slice(0, MAX_DISPLAYED);
37
+
const remainingCollections = collections.slice(
38
+
MAX_DISPLAYED,
39
+
collections.length,
40
+
);
23
41
const remainingCount = collections.length - MAX_DISPLAYED;
24
42
25
43
return (
26
-
<Text fw={500} c={'gray.7'}>
44
+
<Text fw={500} c={'gray'}>
27
45
<Anchor
28
46
component={Link}
29
47
href={`/profile/${props.user.handle}`}
30
-
c="blue"
48
+
c="dark"
31
49
fw={600}
32
50
>
33
51
{sanitizeText(props.user.name)}
···
52
70
</span>
53
71
),
54
72
)}
55
-
{remainingCount > 0 &&
56
-
` and ${remainingCount} other collection${remainingCount > 1 ? 's' : ''}`}
73
+
{remainingCount > 0 && ' and '}
74
+
{remainingCount > 0 && (
75
+
<Menu shadow="sm">
76
+
<Menu.Target>
77
+
<Text
78
+
fw={600}
79
+
c={'blue'}
80
+
style={{ cursor: 'pointer', userSelect: 'none' }}
81
+
span
82
+
>
83
+
{remainingCount} other collection
84
+
{remainingCount > 1 ? 's' : ''}
85
+
</Text>
86
+
</Menu.Target>
87
+
<Menu.Dropdown maw={380}>
88
+
<ScrollArea.Autosize mah={150} type="auto">
89
+
{remainingCollections.map((c) => (
90
+
<Menu.Item
91
+
key={c.id}
92
+
component={Link}
93
+
href={`/profile/${c.author.handle}/collections/${getRecordKey(c.uri!)}`}
94
+
target="_blank"
95
+
c="blue"
96
+
fw={600}
97
+
>
98
+
{c.name}
99
+
</Menu.Item>
100
+
))}
101
+
</ScrollArea.Autosize>
102
+
</Menu.Dropdown>
103
+
</Menu>
104
+
)}
57
105
</Fragment>
58
106
)}
59
107
<Text fz={'sm'} fw={600} c={'gray'} span display={'block'}>
···
64
112
};
65
113
66
114
return (
67
-
<Paper bg={'gray.1'} radius={'lg'}>
115
+
<Card p={0} bg={colorScheme === 'dark' ? 'dark.4' : 'gray.1'} radius={'lg'}>
68
116
<Stack gap={'xs'}>
69
117
<Group gap={'xs'} wrap="nowrap" align="center" p={'xs'}>
70
118
<Avatar
···
76
124
{renderActivityText()}
77
125
</Group>
78
126
</Stack>
79
-
</Paper>
127
+
</Card>
80
128
);
81
129
}
+12
-3
src/webapp/features/feeds/components/feedItem/Skeleton.FeedItem.tsx
+12
-3
src/webapp/features/feeds/components/feedItem/Skeleton.FeedItem.tsx
···
1
+
'use client';
2
+
1
3
import UrlCardSkeleton from '@/features/cards/components/urlCard/Skeleton.UrlCard';
2
-
import { Avatar, Group, Paper, Skeleton, Stack } from '@mantine/core';
4
+
import { Avatar, Card, Group, Paper, Skeleton, Stack } from '@mantine/core';
5
+
import { useColorScheme } from '@mantine/hooks';
3
6
4
7
export default function FeedItemSkeleton() {
8
+
const colorScheme = useColorScheme();
9
+
5
10
return (
6
11
<Stack gap={'xs'} align="stretch">
7
12
{/* Feed activity status*/}
8
-
<Paper bg={'gray.1'} radius={'lg'}>
13
+
<Card
14
+
p={0}
15
+
bg={colorScheme === 'dark' ? 'dark.4' : 'gray.1'}
16
+
radius={'lg'}
17
+
>
9
18
<Stack gap={'xs'} align="stretch" w={'100%'}>
10
19
<Group gap={'xs'} wrap="nowrap" align="center" p={'xs'}>
11
20
<Avatar />
···
15
24
</Stack>
16
25
</Group>
17
26
</Stack>
18
-
</Paper>
27
+
</Card>
19
28
20
29
<UrlCardSkeleton />
21
30
</Stack>
+1
-1
src/webapp/features/notes/components/editNoteDrawer/EditNoteDrawer.tsx
+1
-1
src/webapp/features/notes/components/editNoteDrawer/EditNoteDrawer.tsx
+2
-2
src/webapp/features/notes/components/noteCard/NoteCard.tsx
+2
-2
src/webapp/features/notes/components/noteCard/NoteCard.tsx
···
15
15
const relativeCreateDate = time === 'just now' ? `${time}` : `${time} ago`;
16
16
17
17
return (
18
-
<Card p={'sm'} radius={'lg'} withBorder>
19
-
<Stack>
18
+
<Card p={'sm'} radius={'lg'} h={'100%'} withBorder>
19
+
<Stack justify="space-between" h={'100%'}>
20
20
<Spoiler showLabel={'Read more'} hideLabel={'See less'} maxHeight={200}>
21
21
<Text fs={'italic'}>{props.note}</Text>
22
22
</Spoiler>
+1
-1
src/webapp/features/notes/components/noteCard/Skeleton.NoteCard.tsx
+1
-1
src/webapp/features/notes/components/noteCard/Skeleton.NoteCard.tsx
+72
-7
src/webapp/features/notes/components/noteCardModal/NoteCardModalContent.tsx
+72
-7
src/webapp/features/notes/components/noteCardModal/NoteCardModalContent.tsx
···
14
14
} from '@mantine/core';
15
15
import { UrlCard, User } from '@semble/types';
16
16
import Link from 'next/link';
17
-
import { useState } from 'react';
17
+
import { Fragment, useState } from 'react';
18
18
import useUpdateNote from '../../lib/mutations/useUpdateNote';
19
19
import { notifications } from '@mantine/notifications';
20
+
import useRemoveCardFromLibrary from '@/features/cards/lib/mutations/useRemoveCardFromLibrary';
20
21
21
22
interface Props {
23
+
onClose: () => void;
22
24
note: UrlCard['note'];
23
25
cardContent: UrlCard['cardContent'];
24
26
cardAuthor?: User;
···
30
32
const isMyCard = props.cardAuthor?.id === cardStatus.data.card?.author.id;
31
33
const [note, setNote] = useState(isMyCard ? props.note?.text : '');
32
34
const [editMode, setEditMode] = useState(false);
35
+
const [showDeleteWarning, setShowDeleteWarning] = useState(false);
33
36
37
+
const removeNote = useRemoveCardFromLibrary();
34
38
const updateNote = useUpdateNote();
35
39
40
+
const handleDeleteNote = () => {
41
+
if (!isMyCard || !props.note) return;
42
+
43
+
removeNote.mutate(props.note.id, {
44
+
onError: () => {
45
+
notifications.show({
46
+
message: 'Could not delete note.',
47
+
position: 'top-center',
48
+
});
49
+
},
50
+
onSettled: () => {
51
+
props.onClose();
52
+
},
53
+
});
54
+
};
55
+
36
56
const handleUpdateNote = () => {
37
-
if (!props.note || !note) return;
57
+
if (!props.note || !note) {
58
+
props.onClose();
59
+
return;
60
+
}
61
+
62
+
if (props.note.text === note) {
63
+
props.onClose();
64
+
return;
65
+
}
38
66
39
67
updateNote.mutate(
40
68
{
···
151
179
</Text>
152
180
)}
153
181
</Stack>
154
-
{isMyCard && (
182
+
</Group>
183
+
</Stack>
184
+
</Card>
185
+
{isMyCard && (
186
+
<Fragment>
187
+
{showDeleteWarning ? (
188
+
<Group justify="space-between" gap={'xs'}>
189
+
<Text>Delete note?</Text>
190
+
<Group gap={'xs'}>
191
+
<Button
192
+
color="red"
193
+
onClick={handleDeleteNote}
194
+
loading={removeNote.isPending}
195
+
>
196
+
Delete
197
+
</Button>
198
+
<Button
199
+
variant="light"
200
+
color="gray"
201
+
onClick={() => setShowDeleteWarning(false)}
202
+
>
203
+
Cancel
204
+
</Button>
205
+
</Group>
206
+
</Group>
207
+
) : (
208
+
<Group gap={'xs'} grow>
155
209
<Button
156
210
variant="light"
157
211
color="gray"
···
162
216
>
163
217
Edit note
164
218
</Button>
165
-
)}
166
-
</Group>
167
-
</Stack>
168
-
</Card>
219
+
220
+
<Button
221
+
variant="light"
222
+
color="red"
223
+
onClick={(e) => {
224
+
e.stopPropagation();
225
+
setShowDeleteWarning(true);
226
+
}}
227
+
>
228
+
Delete note
229
+
</Button>
230
+
</Group>
231
+
)}
232
+
</Fragment>
233
+
)}
169
234
</Stack>
170
235
);
171
236
}
+6
-1
src/webapp/features/notes/lib/mutations/useUpdateNote.tsx
+6
-1
src/webapp/features/notes/lib/mutations/useUpdateNote.tsx
···
3
3
import { cardKeys } from '@/features/cards/lib/cardKeys';
4
4
import { collectionKeys } from '@/features/collections/lib/collectionKeys';
5
5
import { feedKeys } from '@/features/feeds/lib/feedKeys';
6
+
import { noteKeys } from '../noteKeys';
6
7
7
8
export default function useUpdateNote() {
8
9
const queryClient = useQueryClient();
···
15
16
onSuccess: (data) => {
16
17
queryClient.invalidateQueries({ queryKey: cardKeys.card(data.cardId) });
17
18
queryClient.invalidateQueries({ queryKey: cardKeys.infinite() });
18
-
queryClient.invalidateQueries({ queryKey: cardKeys.infinite() });
19
+
queryClient.invalidateQueries({
20
+
queryKey: cardKeys.infinite(data.cardId),
21
+
});
22
+
queryClient.invalidateQueries({ queryKey: cardKeys.all() });
23
+
queryClient.invalidateQueries({ queryKey: noteKeys.all() });
19
24
queryClient.invalidateQueries({ queryKey: feedKeys.all() });
20
25
queryClient.invalidateQueries({ queryKey: collectionKeys.all() });
21
26
},
+3
-3
src/webapp/features/profile/components/profileHeader/ProfileHeader.tsx
+3
-3
src/webapp/features/profile/components/profileHeader/ProfileHeader.tsx
···
23
23
const profile = await getProfile(props.handle);
24
24
25
25
return (
26
-
<Container bg={'white'} p={0} size={'xl'}>
26
+
<Container p={0} size={'xl'}>
27
27
<MinimalProfileHeaderContainer
28
28
avatarUrl={profile.avatarUrl}
29
29
name={profile.name}
···
69
69
component="a"
70
70
href={`https://bsky.app/profile/${profile.handle}`}
71
71
target="_blank"
72
+
variant="light"
72
73
radius={'xl'}
73
-
bg="gray.2"
74
-
c={'gray'}
74
+
color={'gray'}
75
75
leftSection={<FaBluesky />}
76
76
>
77
77
{truncateText(profile.handle, 14)}
+1
-1
src/webapp/features/profile/components/profileHeader/Skeleton.ProfileHeader.tsx
+1
-1
src/webapp/features/profile/components/profileHeader/Skeleton.ProfileHeader.tsx
+58
-8
src/webapp/features/profile/components/profileMenu/ProfileMenu.tsx
+58
-8
src/webapp/features/profile/components/profileMenu/ProfileMenu.tsx
···
6
6
Menu,
7
7
Image,
8
8
Button,
9
+
useMantineColorScheme,
10
+
useComputedColorScheme,
9
11
} from '@mantine/core';
10
12
import useMyProfile from '../../lib/queries/useMyProfile';
11
13
import CosmikLogo from '@/assets/cosmik-logo-full.svg';
12
-
import { MdBugReport } from 'react-icons/md';
14
+
import CosmikLogoWhite from '@/assets/cosmik-logo-full-white.svg';
15
+
import {
16
+
MdBugReport,
17
+
MdDarkMode,
18
+
MdLightMode,
19
+
MdAutoAwesome,
20
+
} from 'react-icons/md';
13
21
import { useAuth } from '@/hooks/useAuth';
14
22
import { useRouter } from 'next/navigation';
15
23
import Link from 'next/link';
···
23
31
const { data, error, isPending } = useMyProfile();
24
32
const { logout } = useAuth();
25
33
34
+
const { colorScheme, setColorScheme } = useMantineColorScheme();
35
+
const computedColorScheme = useComputedColorScheme('light', {
36
+
getInitialValueInEffect: true,
37
+
});
38
+
26
39
const handleLogout = async () => {
27
40
try {
28
41
await logout();
···
32
45
}
33
46
};
34
47
48
+
const handleThemeToggle = () => {
49
+
const nextScheme =
50
+
colorScheme === 'light'
51
+
? 'dark'
52
+
: colorScheme === 'dark'
53
+
? 'auto'
54
+
: 'light';
55
+
56
+
setColorScheme(nextScheme);
57
+
};
58
+
35
59
if (isPending || !data) {
36
-
return <Skeleton w={38} h={38} radius={'md'} ml={4} />;
60
+
return <Skeleton w={38} h={38} radius="md" ml={4} />;
37
61
}
38
62
39
63
if (error) {
···
46
70
<Menu.Target>
47
71
<Button
48
72
variant="subtle"
49
-
color="gray"
50
-
c={'dark'}
51
-
fz={'md'}
52
-
radius={'md'}
73
+
color={computedColorScheme === 'dark' ? 'gray' : 'dark'}
74
+
fz="md"
75
+
radius="md"
53
76
size="lg"
54
77
px={3}
55
-
fullWidth={true}
78
+
fullWidth
56
79
justify="start"
57
80
leftSection={<Avatar src={data.avatarUrl} />}
58
81
>
59
82
{data.name}
60
83
</Button>
61
84
</Menu.Target>
85
+
62
86
<Menu.Dropdown>
63
87
<Menu.Item
64
88
component={Link}
···
90
114
91
115
<Menu.Divider />
92
116
117
+
{/*<Menu.Item
118
+
color="gray"
119
+
leftSection={
120
+
colorScheme === 'auto' ? (
121
+
<MdAutoAwesome />
122
+
) : computedColorScheme === 'dark' ? (
123
+
<MdDarkMode />
124
+
) : (
125
+
<MdLightMode />
126
+
)
127
+
}
128
+
closeMenuOnClick={false}
129
+
onClick={handleThemeToggle}
130
+
>
131
+
Theme: {colorScheme}
132
+
</Menu.Item>*/}
133
+
93
134
<Menu.Item
94
135
component="a"
95
136
href="https://cosmik.network/"
96
137
target="_blank"
97
138
>
98
-
<Image src={CosmikLogo.src} alt="Cosmik logo" w={'auto'} h={24} />
139
+
<Image
140
+
src={
141
+
computedColorScheme === 'dark'
142
+
? CosmikLogoWhite.src
143
+
: CosmikLogo.src
144
+
}
145
+
alt="Cosmik logo"
146
+
w="auto"
147
+
h={24}
148
+
/>
99
149
</Menu.Item>
100
150
</Menu.Dropdown>
101
151
</Menu>
+18
-16
src/webapp/features/profile/components/profileTabs/ProfileTabs.tsx
+18
-16
src/webapp/features/profile/components/profileTabs/ProfileTabs.tsx
···
1
1
'use client';
2
2
3
-
import { Group, ScrollAreaAutosize, Tabs } from '@mantine/core';
3
+
import { Group, Paper, ScrollAreaAutosize, Tabs } from '@mantine/core';
4
4
import TabItem from './TabItem';
5
5
import { usePathname } from 'next/navigation';
6
6
···
16
16
17
17
return (
18
18
<Tabs value={currentTab}>
19
-
<ScrollAreaAutosize type="scroll">
20
-
<Tabs.List>
21
-
<Group wrap="nowrap">
22
-
<TabItem value="profile" href={basePath}>
23
-
Profile
24
-
</TabItem>
25
-
<TabItem value="cards" href={`${basePath}/cards`}>
26
-
Cards
27
-
</TabItem>
28
-
<TabItem value="collections" href={`${basePath}/collections`}>
29
-
Collections
30
-
</TabItem>
31
-
</Group>
32
-
</Tabs.List>
33
-
</ScrollAreaAutosize>
19
+
<Paper radius={0}>
20
+
<ScrollAreaAutosize type="scroll">
21
+
<Tabs.List>
22
+
<Group wrap="nowrap">
23
+
<TabItem value="profile" href={basePath}>
24
+
Profile
25
+
</TabItem>
26
+
<TabItem value="cards" href={`${basePath}/cards`}>
27
+
Cards
28
+
</TabItem>
29
+
<TabItem value="collections" href={`${basePath}/collections`}>
30
+
Collections
31
+
</TabItem>
32
+
</Group>
33
+
</Tabs.List>
34
+
</ScrollAreaAutosize>
35
+
</Paper>
34
36
</Tabs>
35
37
);
36
38
}
+14
-7
src/webapp/features/profile/components/profileTabs/TabItem.tsx
+14
-7
src/webapp/features/profile/components/profileTabs/TabItem.tsx
···
1
-
import { Anchor, Tabs } from '@mantine/core';
1
+
'use client';
2
+
3
+
import { Tabs } from '@mantine/core';
2
4
import classes from './TabItem.module.css';
3
-
import Link from 'next/link';
5
+
import { useRouter } from 'next/navigation';
4
6
5
7
interface Props {
6
8
value: string;
···
9
11
}
10
12
11
13
export default function TabItem(props: Props) {
14
+
const router = useRouter();
15
+
12
16
return (
13
-
<Anchor component={Link} href={props.href} c={'dark'} underline="never">
14
-
<Tabs.Tab value={props.value} className={classes.tab} fw={600}>
15
-
{props.children}
16
-
</Tabs.Tab>
17
-
</Anchor>
17
+
<Tabs.Tab
18
+
value={props.value}
19
+
className={classes.tab}
20
+
fw={600}
21
+
onClick={() => router.push(props.href)}
22
+
>
23
+
{props.children}
24
+
</Tabs.Tab>
18
25
);
19
26
}
+10
-2
src/webapp/features/semble/components/SembleHeader/SembleHeader.tsx
+10
-2
src/webapp/features/semble/components/SembleHeader/SembleHeader.tsx
···
9
9
Tooltip,
10
10
Spoiler,
11
11
Card,
12
+
Button,
12
13
} from '@mantine/core';
13
14
import Link from 'next/link';
14
15
import { getUrlMetadata } from '@/features/cards/lib/dal';
15
16
import { getDomain } from '@/lib/utils/link';
16
17
import UrlAddedBySummary from '../urlAddedBySummary/UrlAddedBySummary';
17
-
// import SembleActions from '../sembleActions/SembleActions';
18
+
import SembleActions from '../sembleActions/SembleActions';
19
+
import { verifySessionOnServer } from '@/lib/auth/dal.server';
20
+
import GuestSembleActions from '../sembleActions/GusetSembleActions';
18
21
19
22
interface Props {
20
23
url: string;
···
22
25
23
26
export default async function SembleHeader(props: Props) {
24
27
const { metadata } = await getUrlMetadata(props.url);
28
+
const session = await verifySessionOnServer();
25
29
26
30
return (
27
31
<Stack gap={'xl'}>
···
74
78
/>
75
79
</Card>
76
80
)}
77
-
{/*<SembleActions url={props.url} />*/}
81
+
{session ? (
82
+
<SembleActions url={props.url} />
83
+
) : (
84
+
<GuestSembleActions url={props.url} />
85
+
)}
78
86
</Stack>
79
87
</GridCol>
80
88
</Grid>
+6
-2
src/webapp/features/semble/components/SembleHeader/Skeleton.SembleHeader.tsx
+6
-2
src/webapp/features/semble/components/SembleHeader/Skeleton.SembleHeader.tsx
···
1
-
import { Stack, Grid, GridCol, Text, Skeleton } from '@mantine/core';
1
+
import { Stack, Grid, GridCol, Text, Skeleton, Group } from '@mantine/core';
2
2
import UrlAddedBySummarySkeleton from '../urlAddedBySummary/Skeleton.UrlAddedBySummary';
3
3
4
4
export default function SembleHeaderSkeleton() {
···
25
25
</Stack>
26
26
</GridCol>
27
27
<GridCol span={{ base: 12, sm: 'content' }}>
28
-
<Stack gap={'sm'} align="start" flex={1}>
28
+
<Stack gap={'sm'} align="center" flex={1}>
29
29
<Skeleton h={150} w={300} maw={'100%'} />
30
30
31
31
{/*<SembleActions />*/}
32
+
<Group gap={'xs'}>
33
+
<Skeleton w={44} h={44} circle />
34
+
<Skeleton w={131} h={44} radius={'xl'} />
35
+
</Group>
32
36
</Stack>
33
37
</GridCol>
34
38
</Grid>
+53
src/webapp/features/semble/components/sembleActions/GusetSembleActions.tsx
+53
src/webapp/features/semble/components/sembleActions/GusetSembleActions.tsx
···
1
+
'use client';
2
+
3
+
import { ActionIcon, Button, CopyButton, Group, Tooltip } from '@mantine/core';
4
+
import Link from 'next/link';
5
+
import { MdIosShare } from 'react-icons/md';
6
+
import { notifications } from '@mantine/notifications';
7
+
8
+
interface Props {
9
+
url: string;
10
+
}
11
+
12
+
export default function GuestSembleActions(props: Props) {
13
+
const shareLink =
14
+
typeof window !== 'undefined'
15
+
? `${window.location.origin}/url?id=${props.url}`
16
+
: '';
17
+
18
+
return (
19
+
<Group gap={'xs'}>
20
+
<CopyButton value={shareLink}>
21
+
{({ copied, copy }) => (
22
+
<Tooltip
23
+
label={copied ? 'Link copied!' : 'Share'}
24
+
withArrow
25
+
position="top"
26
+
>
27
+
<ActionIcon
28
+
variant="light"
29
+
color="gray"
30
+
size={'xl'}
31
+
radius={'xl'}
32
+
onClick={() => {
33
+
copy();
34
+
35
+
if (copied) return;
36
+
notifications.show({
37
+
message: 'Link copied!',
38
+
position: 'top-center',
39
+
id: copied.toString(),
40
+
});
41
+
}}
42
+
>
43
+
<MdIosShare size={22} />
44
+
</ActionIcon>
45
+
</Tooltip>
46
+
)}
47
+
</CopyButton>
48
+
<Button size="md" component={Link} href={'/login'}>
49
+
Log in to add
50
+
</Button>
51
+
</Group>
52
+
);
53
+
}
+51
-3
src/webapp/features/semble/components/sembleActions/SembleActions.tsx
+51
-3
src/webapp/features/semble/components/sembleActions/SembleActions.tsx
···
1
1
'use client';
2
2
3
+
import AddCardToModal from '@/features/cards/components/addCardToModal/AddCardToModal';
3
4
import useGetCardFromMyLibrary from '@/features/cards/lib/queries/useGetCardFromMyLibrary';
4
-
import { Button, Group } from '@mantine/core';
5
-
import { Fragment } from 'react';
5
+
import { ActionIcon, Button, CopyButton, Group, Tooltip } from '@mantine/core';
6
+
import { notifications } from '@mantine/notifications';
7
+
import { Fragment, useState } from 'react';
6
8
import { FiPlus } from 'react-icons/fi';
7
9
import { IoMdCheckmark } from 'react-icons/io';
10
+
import { MdIosShare } from 'react-icons/md';
8
11
9
12
interface Props {
10
13
url: string;
···
13
16
export default function SembleActions(props: Props) {
14
17
const cardStatus = useGetCardFromMyLibrary({ url: props.url });
15
18
const isInYourLibrary = cardStatus.data.card?.urlInLibrary;
19
+
const [showAddToModal, setShowAddToModal] = useState(false);
20
+
21
+
const shareLink =
22
+
typeof window !== 'undefined'
23
+
? `${window.location.origin}/url?id=${props.url}`
24
+
: '';
16
25
17
26
if (cardStatus.error) {
18
27
return null;
···
20
29
21
30
return (
22
31
<Fragment>
23
-
<Group>
32
+
<Group gap={'xs'}>
33
+
<CopyButton value={shareLink}>
34
+
{({ copied, copy }) => (
35
+
<Tooltip
36
+
label={copied ? 'Link copied!' : 'Share'}
37
+
withArrow
38
+
position="top"
39
+
>
40
+
<ActionIcon
41
+
variant="light"
42
+
color="gray"
43
+
size={'xl'}
44
+
radius={'xl'}
45
+
onClick={() => {
46
+
copy();
47
+
48
+
if (copied) return;
49
+
notifications.show({
50
+
message: 'Link copied!',
51
+
position: 'top-center',
52
+
id: copied.toString(),
53
+
});
54
+
}}
55
+
>
56
+
<MdIosShare size={22} />
57
+
</ActionIcon>
58
+
</Tooltip>
59
+
)}
60
+
</CopyButton>
24
61
<Button
25
62
variant={isInYourLibrary ? 'default' : 'filled'}
26
63
size="md"
27
64
leftSection={
28
65
isInYourLibrary ? <IoMdCheckmark size={18} /> : <FiPlus size={18} />
29
66
}
67
+
onClick={() => setShowAddToModal(true)}
30
68
>
31
69
{isInYourLibrary ? 'In library' : 'Add to library'}
32
70
</Button>
33
71
</Group>
72
+
73
+
<AddCardToModal
74
+
isOpen={showAddToModal}
75
+
onClose={() => setShowAddToModal(false)}
76
+
url={props.url}
77
+
cardId={cardStatus.data.card?.id}
78
+
note={cardStatus.data.card?.note?.text}
79
+
urlLibraryCount={cardStatus.data.card?.urlLibraryCount}
80
+
isInYourLibrary={cardStatus.data.card?.urlInLibrary}
81
+
/>
34
82
</Fragment>
35
83
);
36
84
}
+1
-1
src/webapp/features/semble/components/sembleTabs/TabItem.tsx
+1
-1
src/webapp/features/semble/components/sembleTabs/TabItem.tsx
+1
-1
src/webapp/features/semble/components/urlAddedBySummary/Skeleton.UrlAddedBySummary.tsx
+1
-1
src/webapp/features/semble/components/urlAddedBySummary/Skeleton.UrlAddedBySummary.tsx
+8
-10
src/webapp/features/semble/containers/sembleCollectionsContainer/Skeleton.SembleCollectionsContainer.tsx
+8
-10
src/webapp/features/semble/containers/sembleCollectionsContainer/Skeleton.SembleCollectionsContainer.tsx
···
1
-
import { Container, SimpleGrid, Stack } from '@mantine/core';
1
+
import { SimpleGrid, Stack } from '@mantine/core';
2
2
import CollectionCardSkeleton from '@/features/collections/components/collectionCard/Skeleton.CollectionCard';
3
3
4
4
export default function SembleCollectionsContainerSkeleton() {
5
5
return (
6
-
<Container p="xs" size="xl">
7
-
<Stack>
8
-
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="md">
9
-
{Array.from({ length: 4 }).map((_, i) => (
10
-
<CollectionCardSkeleton key={i} />
11
-
))}
12
-
</SimpleGrid>
13
-
</Stack>
14
-
</Container>
6
+
<Stack>
7
+
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="md">
8
+
{Array.from({ length: 4 }).map((_, i) => (
9
+
<CollectionCardSkeleton key={i} />
10
+
))}
11
+
</SimpleGrid>
12
+
</Stack>
15
13
);
16
14
}
+3
-24
src/webapp/features/semble/containers/sembleContainer/SembleContainer.tsx
+3
-24
src/webapp/features/semble/containers/sembleContainer/SembleContainer.tsx
···
1
1
import SembleHeader from '../../components/SembleHeader/SembleHeader';
2
-
import { Image, Container, Stack, Box } from '@mantine/core';
3
-
import BG from '@/assets/semble-header-bg.webp';
2
+
import { Container, Stack } from '@mantine/core';
4
3
import { Suspense } from 'react';
5
4
import SembleTabs from '../../components/sembleTabs/SembleTabs';
6
5
import SembleHeaderSkeleton from '../../components/SembleHeader/Skeleton.SembleHeader';
6
+
import SembleHeaderBackground from './SembleHeaderBackground';
7
7
8
8
interface Props {
9
9
url: string;
···
12
12
export default async function SembleContainer(props: Props) {
13
13
return (
14
14
<Container p={0} fluid>
15
-
<Box style={{ position: 'relative', width: '100%' }}>
16
-
<Image
17
-
src={BG.src}
18
-
alt="bg"
19
-
fit="cover"
20
-
w="100%"
21
-
h={{ base: 100, md: 120 }}
22
-
/>
23
-
24
-
{/* White gradient overlay */}
25
-
<Box
26
-
style={{
27
-
position: 'absolute',
28
-
bottom: 0,
29
-
left: 0,
30
-
width: '100%',
31
-
height: '60%', // fade height
32
-
background: 'linear-gradient(to top, white, transparent)',
33
-
pointerEvents: 'none',
34
-
}}
35
-
/>
36
-
</Box>
15
+
<SembleHeaderBackground />
37
16
<Container px={'xs'} pb={'xs'} size={'xl'}>
38
17
<Stack gap={'xl'}>
39
18
<Suspense fallback={<SembleHeaderSkeleton />}>
+36
src/webapp/features/semble/containers/sembleContainer/SembleHeaderBackground.tsx
+36
src/webapp/features/semble/containers/sembleContainer/SembleHeaderBackground.tsx
···
1
+
'use client';
2
+
3
+
import { useColorScheme } from '@mantine/hooks';
4
+
import BG from '@/assets/semble-header-bg.webp';
5
+
import DarkBG from '@/assets/semble-header-bg-dark.webp';
6
+
import { Box, Image } from '@mantine/core';
7
+
8
+
export default function SembleHeaderBackground() {
9
+
const colorScheme = useColorScheme();
10
+
11
+
return (
12
+
<Box style={{ position: 'relative', width: '100%' }}>
13
+
<Image
14
+
src={colorScheme === 'dark' ? DarkBG.src : BG.src}
15
+
alt="bg"
16
+
fit="cover"
17
+
w="100%"
18
+
h={80}
19
+
/>
20
+
21
+
{/* White gradient overlay */}
22
+
<Box
23
+
style={{
24
+
position: 'absolute',
25
+
bottom: 0,
26
+
left: 0,
27
+
width: '100%',
28
+
height: '60%', // fade height
29
+
background:
30
+
'linear-gradient(to top, var(--mantine-color-body), transparent)',
31
+
pointerEvents: 'none',
32
+
}}
33
+
/>
34
+
</Box>
35
+
);
36
+
}
+16
src/webapp/features/semble/containers/sembleContainer/Skeleton.SembleContainer.tsx
+16
src/webapp/features/semble/containers/sembleContainer/Skeleton.SembleContainer.tsx
···
1
+
import { Container, Stack } from '@mantine/core';
2
+
import SembleHeaderSkeleton from '../../components/SembleHeader/Skeleton.SembleHeader';
3
+
import SembleHeaderBackground from './SembleHeaderBackground';
4
+
5
+
export default function SembleContainerSkeleton() {
6
+
return (
7
+
<Container p={0} fluid>
8
+
<SembleHeaderBackground />
9
+
<Container px={'xs'} pb={'xs'} size={'xl'}>
10
+
<Stack gap={'xl'}>
11
+
<SembleHeaderSkeleton />
12
+
</Stack>
13
+
</Container>
14
+
</Container>
15
+
);
16
+
}
+1
-1
src/webapp/providers/mantine.tsx
+1
-1
src/webapp/providers/mantine.tsx
+4
-1
src/webapp/styles/theme.tsx
+4
-1
src/webapp/styles/theme.tsx