src
webapp
app
(authenticated)
auth
login
components
features
cards
components
containers
lib
collections
components
collectionSelector
collectionsNavList
containers
collectionContainer
collectionsContainer
lib
feeds
library
···
29
29
const params = useParams();
30
30
const cardId = params.cardId as string;
31
31
32
32
-
// Create API client instance
33
33
-
const apiClient = new ApiClient(
34
34
-
process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000',
35
35
-
() => getAccessToken(),
36
36
-
);
37
37
-
38
32
useEffect(() => {
33
33
+
// Create API client instance
34
34
+
const apiClient = new ApiClient(
35
35
+
process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000',
36
36
+
() => getAccessToken(),
37
37
+
);
38
38
+
39
39
const fetchCard = async () => {
40
40
try {
41
41
setLoading(true);
···
1
1
+
'use client';
2
2
+
3
3
+
import MyFeedContainerError from '@/features/feeds/containers/myFeedContainer/Error.MyFeedContainer';
4
4
+
5
5
+
export default function Error() {
6
6
+
return <MyFeedContainerError />;
7
7
+
}
···
1
1
+
import MyFeedContainerSkeleton from '@/features/feeds/containers/myFeedContainer/Skeleton.MyFeedContainer';
2
2
+
3
3
+
export default function Loading() {
4
4
+
return <MyFeedContainerSkeleton />;
5
5
+
}
···
1
1
-
'use client';
2
2
-
3
3
-
import { useEffect, useState, useMemo, useCallback } from 'react';
4
4
-
import { ApiClient } from '@/api-client/ApiClient';
5
5
-
import { getAccessToken } from '@/services/auth';
6
6
-
import FeedItem from '@/features/feeds/components/feedItem/FeedItem';
7
7
-
import type { GetGlobalFeedResponse } from '@/api-client/types';
8
8
-
import {
9
9
-
Button,
10
10
-
Loader,
11
11
-
Stack,
12
12
-
Title,
13
13
-
Text,
14
14
-
Center,
15
15
-
Container,
16
16
-
} from '@mantine/core';
17
17
-
18
18
-
export default function ExplorePage() {
19
19
-
const [feedItems, setFeedItems] = useState<
20
20
-
GetGlobalFeedResponse['activities']
21
21
-
>([]);
22
22
-
const [loading, setLoading] = useState(true);
23
23
-
const [loadingMore, setLoadingMore] = useState(false);
24
24
-
const [hasMore, setHasMore] = useState(true);
25
25
-
const [error, setError] = useState<string | null>(null);
26
26
-
27
27
-
// Memoize API client instance
28
28
-
const apiClient = useMemo(
29
29
-
() =>
30
30
-
new ApiClient(
31
31
-
process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000',
32
32
-
() => getAccessToken(),
33
33
-
),
34
34
-
[],
35
35
-
);
36
36
-
37
37
-
// Fetch initial feed data
38
38
-
const fetchFeed = useCallback(
39
39
-
async (reset = false) => {
40
40
-
try {
41
41
-
if (reset) {
42
42
-
setLoading(true);
43
43
-
setError(null);
44
44
-
} else {
45
45
-
setLoadingMore(true);
46
46
-
}
47
47
-
48
48
-
const response = await apiClient.getGlobalFeed({
49
49
-
limit: 20,
50
50
-
beforeActivityId: reset
51
51
-
? undefined
52
52
-
: feedItems[feedItems.length - 1]?.id,
53
53
-
});
54
54
-
55
55
-
if (reset) {
56
56
-
setFeedItems(response.activities);
57
57
-
} else {
58
58
-
setFeedItems((prev) => [...prev, ...response.activities]);
59
59
-
}
60
60
-
61
61
-
setHasMore(response.pagination.hasMore);
62
62
-
} catch (error: any) {
63
63
-
console.error('Error fetching feed:', error);
64
64
-
setError(error.message || 'Failed to load feed');
65
65
-
} finally {
66
66
-
setLoading(false);
67
67
-
setLoadingMore(false);
68
68
-
}
69
69
-
},
70
70
-
[apiClient, feedItems],
71
71
-
);
72
72
-
73
73
-
useEffect(() => {
74
74
-
fetchFeed(true);
75
75
-
}, [apiClient]); // Only depend on apiClient, not fetchFeed to avoid infinite loop
76
76
-
77
77
-
const handleLoadMore = () => {
78
78
-
if (!loadingMore && hasMore) {
79
79
-
fetchFeed(false);
80
80
-
}
81
81
-
};
82
82
-
83
83
-
const handleRetry = () => {
84
84
-
fetchFeed(true);
85
85
-
};
86
86
-
87
87
-
if (loading) {
88
88
-
return <Loader />;
89
89
-
}
90
90
-
91
91
-
if (error) {
92
92
-
return (
93
93
-
<Center h={200}>
94
94
-
<Stack align="center">
95
95
-
<Text c="red">{error}</Text>
96
96
-
<Button onClick={handleRetry} variant="outline">
97
97
-
Try Again
98
98
-
</Button>
99
99
-
</Stack>
100
100
-
</Center>
101
101
-
);
102
102
-
}
1
1
+
import MyFeedContainer from '@/features/feeds/containers/myFeedContainer/MyFeedContainer';
103
2
104
104
-
return (
105
105
-
<Container p={'xs'} size={'xl'}>
106
106
-
<Stack>
107
107
-
<Title order={2}>Explore</Title>
108
108
-
109
109
-
{feedItems.length === 0 ? (
110
110
-
<Center h={200}>
111
111
-
<Text c="dimmed">No activity to show yet</Text>
112
112
-
</Center>
113
113
-
) : (
114
114
-
<Stack gap="xl">
115
115
-
{feedItems.map((item) => (
116
116
-
<FeedItem key={item.id} item={item} />
117
117
-
))}
118
118
-
119
119
-
{hasMore && (
120
120
-
<Center>
121
121
-
<Button
122
122
-
onClick={handleLoadMore}
123
123
-
loading={loadingMore}
124
124
-
variant="outline"
125
125
-
>
126
126
-
{loadingMore ? 'Loading...' : 'Load More'}
127
127
-
</Button>
128
128
-
</Center>
129
129
-
)}
130
130
-
</Stack>
131
131
-
)}
132
132
-
</Stack>
133
133
-
</Container>
134
134
-
);
3
3
+
export default function Page() {
4
4
+
return <MyFeedContainer />;
135
5
}
···
14
14
const searchParams = useSearchParams();
15
15
const { setTokens } = useAuth();
16
16
17
17
-
// Create API client instance
18
18
-
const apiClient = new ApiClient(
19
19
-
process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000',
20
20
-
() => getAccessToken(),
21
21
-
);
22
22
-
23
17
useEffect(() => {
18
18
+
// Create API client instance
19
19
+
const apiClient = new ApiClient(
20
20
+
process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000',
21
21
+
() => getAccessToken(),
22
22
+
);
23
23
+
24
24
const accessToken = searchParams.get('accessToken');
25
25
const refreshToken = searchParams.get('refreshToken');
26
26
const error = searchParams.get('error');
···
35
35
return;
36
36
}
37
37
38
38
+
const handleExtensionTokenGeneration = async () => {
39
39
+
try {
40
40
+
setMessage('Generating extension tokens...');
41
41
+
42
42
+
const tokens = await apiClient.generateExtensionTokens();
43
43
+
await ExtensionService.sendTokensToExtension(tokens);
44
44
+
ExtensionService.clearExtensionTokensRequested();
45
45
+
46
46
+
setMessage('Extension tokens generated successfully!');
47
47
+
48
48
+
// Redirect to extension success page after successful extension token generation
49
49
+
setTimeout(() => router.push('/extension/auth/complete'), 1000);
50
50
+
} catch (extensionError: any) {
51
51
+
console.error('Failed to generate extension tokens:', extensionError);
52
52
+
ExtensionService.clearExtensionTokensRequested();
53
53
+
54
54
+
// Redirect to extension error page
55
55
+
router.push('/extension/auth/error');
56
56
+
}
57
57
+
};
58
58
+
38
59
if (accessToken && refreshToken) {
39
60
// Store tokens using the auth context function
40
61
setTokens(accessToken, refreshToken);
···
50
71
router.push('/login?error=Authentication failed');
51
72
}
52
73
}, [router, searchParams, setTokens]);
53
53
-
54
54
-
const handleExtensionTokenGeneration = async () => {
55
55
-
try {
56
56
-
setMessage('Generating extension tokens...');
57
57
-
58
58
-
const tokens = await apiClient.generateExtensionTokens();
59
59
-
await ExtensionService.sendTokensToExtension(tokens);
60
60
-
ExtensionService.clearExtensionTokensRequested();
61
61
-
62
62
-
setMessage('Extension tokens generated successfully!');
63
63
-
64
64
-
// Redirect to extension success page after successful extension token generation
65
65
-
setTimeout(() => router.push('/extension/auth/complete'), 1000);
66
66
-
} catch (extensionError: any) {
67
67
-
console.error('Failed to generate extension tokens:', extensionError);
68
68
-
ExtensionService.clearExtensionTokensRequested();
69
69
-
70
70
-
// Redirect to extension error page
71
71
-
router.push('/extension/auth/error');
72
72
-
}
73
73
-
};
74
74
75
75
return (
76
76
<Stack align="center">
···
44
44
clearTimeout(timeoutId);
45
45
}
46
46
};
47
47
-
}, [isAuthenticated, router]);
47
47
+
}, [isAuthenticated, router, isExtensionLogin]);
48
48
49
49
if (isLoading) {
50
50
return (
···
1
1
'use client';
2
2
3
3
-
import { useState, useEffect, useMemo } from 'react';
3
3
+
import { useState, useEffect, useMemo, useCallback } from 'react';
4
4
import { getAccessToken } from '@/services/auth';
5
5
import { ApiClient } from '@/api-client/ApiClient';
6
6
import { Button, Group, Modal, Stack, Text } from '@mantine/core';
···
47
47
return card.collections || [];
48
48
}, [card]);
49
49
50
50
-
useEffect(() => {
51
51
-
if (isOpen) {
52
52
-
fetchCard();
53
53
-
}
54
54
-
}, [isOpen, cardId]);
50
50
+
const fetchCard = useCallback(async () => {
51
51
+
// Create API client instance
52
52
+
const apiClient = new ApiClient(
53
53
+
process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000',
54
54
+
() => getAccessToken(),
55
55
+
);
55
56
56
56
-
const fetchCard = async () => {
57
57
try {
58
58
setLoading(true);
59
59
setError('');
···
65
65
} finally {
66
66
setLoading(false);
67
67
}
68
68
-
};
68
68
+
}, [cardId]);
69
69
+
70
70
+
useEffect(() => {
71
71
+
if (isOpen) {
72
72
+
fetchCard();
73
73
+
}
74
74
+
}, [isOpen, cardId, fetchCard]);
69
75
70
76
const handleSubmit = async () => {
71
77
if (selectedCollectionIds.length === 0) {
···
77
83
setError('');
78
84
79
85
try {
86
86
+
// Create API client instance
87
87
+
const apiClient = new ApiClient(
88
88
+
process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000',
89
89
+
() => getAccessToken(),
90
90
+
);
91
91
+
80
92
// Add card to all selected collections in a single request
81
93
await apiClient.addCardToCollection({
82
94
cardId,
···
82
82
if (isOpen && initialName !== form.getValues().name) {
83
83
form.setFieldValue('name', initialName);
84
84
}
85
85
-
}, [isOpen, initialName]);
85
85
+
}, [isOpen, initialName, form]);
86
86
87
87
return (
88
88
<Modal
···
1
1
+
'use client';
2
2
+
1
3
import { UrlCardView } from '@/api-client/types';
2
4
import { AddToCollectionModal } from '@/components/AddToCollectionModal';
3
5
import EditNoteDrawer from '@/features/notes/components/editNoteDrawer/EditNoteDrawer';
···
1
1
'use client';
2
2
3
3
-
import { Container, Grid, Stack, Title, Button, Text } from '@mantine/core';
3
3
+
import {
4
4
+
Container,
5
5
+
Grid,
6
6
+
Stack,
7
7
+
Title,
8
8
+
Button,
9
9
+
Text,
10
10
+
Center,
11
11
+
} from '@mantine/core';
4
12
import useMyCards from '../../lib/queries/useMyCards';
5
13
import UrlCard from '@/features/cards/components/urlCard/UrlCard';
6
14
import { BiPlus } from 'react-icons/bi';
7
7
-
import Link from 'next/link';
8
15
import AddCardDrawer from '../../components/addCardDrawer/AddCardDrawer';
9
9
-
import { useState } from 'react';
16
16
+
import { Fragment, useState } from 'react';
17
17
+
import MyCardsContainerError from './Error.MyCardsContainer';
18
18
+
import MyCardsContainerSkeleton from './Skeleton.MyCardsContainer';
10
19
11
20
export default function MyCardsContainer() {
12
12
-
const { data } = useMyCards();
21
21
+
const {
22
22
+
data,
23
23
+
error,
24
24
+
fetchNextPage,
25
25
+
hasNextPage,
26
26
+
isFetchingNextPage,
27
27
+
isPending,
28
28
+
} = useMyCards();
29
29
+
13
30
const [showAddDrawer, setShowAddDrawer] = useState(false);
31
31
+
32
32
+
const allCards = data?.pages.flatMap((page) => page.cards ?? []) ?? [];
33
33
+
34
34
+
if (isPending) {
35
35
+
return <MyCardsContainerSkeleton />;
36
36
+
}
37
37
+
38
38
+
if (error) {
39
39
+
return <MyCardsContainerError />;
40
40
+
}
14
41
15
42
return (
16
16
-
<Container p={'xs'} size={'xl'}>
43
43
+
<Container p="xs" size="xl">
17
44
<Stack>
18
45
<Title order={1}>My Cards</Title>
19
19
-
{data.cards.length > 0 ? (
20
20
-
<Grid gutter={'md'}>
21
21
-
{data.cards.map((card) => (
22
22
-
<Grid.Col key={card.id} span={{ base: 12, xs: 6, sm: 4, lg: 3 }}>
23
23
-
<UrlCard
24
24
-
id={card.id}
25
25
-
url={card.url}
26
26
-
cardContent={card.cardContent}
27
27
-
note={card.note}
28
28
-
collections={card.collections}
29
29
-
/>
30
30
-
</Grid.Col>
31
31
-
))}
32
32
-
</Grid>
46
46
+
47
47
+
{allCards.length > 0 ? (
48
48
+
<Fragment>
49
49
+
<Grid gutter="md">
50
50
+
{allCards.map((card) => (
51
51
+
<Grid.Col
52
52
+
key={card.id}
53
53
+
span={{ base: 12, xs: 6, sm: 4, lg: 3 }}
54
54
+
>
55
55
+
<UrlCard
56
56
+
id={card.id}
57
57
+
url={card.url}
58
58
+
cardContent={card.cardContent}
59
59
+
note={card.note}
60
60
+
collections={card.collections}
61
61
+
/>
62
62
+
</Grid.Col>
63
63
+
))}
64
64
+
</Grid>
65
65
+
66
66
+
{hasNextPage && (
67
67
+
<Center>
68
68
+
<Button
69
69
+
onClick={() => fetchNextPage()}
70
70
+
disabled={isFetchingNextPage}
71
71
+
loading={isFetchingNextPage}
72
72
+
variant="light"
73
73
+
color="gray"
74
74
+
mt="md"
75
75
+
>
76
76
+
Load More
77
77
+
</Button>
78
78
+
</Center>
79
79
+
)}
80
80
+
</Fragment>
33
81
) : (
34
34
-
<Stack align="center" gap={'xs'}>
35
35
-
<Text fz={'h3'} fw={600} c={'gray'}>
82
82
+
<Stack align="center" gap="xs">
83
83
+
<Text fz="h3" fw={600} c="gray">
36
84
No cards
37
85
</Text>
38
86
<Button
39
87
variant="light"
40
40
-
color={'gray'}
88
88
+
color="gray"
41
89
size="md"
42
90
rightSection={<BiPlus size={22} />}
43
91
onClick={() => setShowAddDrawer(true)}
···
1
1
import { ApiClient } from '@/api-client/ApiClient';
2
2
import { getAccessToken } from '@/services/auth';
3
3
-
import { useSuspenseQuery } from '@tanstack/react-query';
3
3
+
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
4
4
5
5
interface Props {
6
6
limit?: number;
···
12
12
() => getAccessToken(),
13
13
);
14
14
15
15
-
const myCards = useSuspenseQuery({
16
16
-
queryKey: ['my cards', props?.limit],
17
17
-
queryFn: () => apiClient.getMyUrlCards({ limit: props?.limit ?? 10 }),
15
15
+
const limit = props?.limit ?? 15;
16
16
+
17
17
+
const query = useSuspenseInfiniteQuery({
18
18
+
queryKey: ['my cards', limit],
19
19
+
initialPageParam: 1,
20
20
+
queryFn: ({ pageParam = 1 }) => {
21
21
+
return apiClient.getMyUrlCards({ limit, page: pageParam });
22
22
+
},
23
23
+
getNextPageParam: (lastPage) => {
24
24
+
if (lastPage.pagination.hasMore) {
25
25
+
return lastPage.pagination.currentPage + 1;
26
26
+
}
27
27
+
return undefined;
28
28
+
},
18
29
});
19
30
20
20
-
return myCards;
31
31
+
return query;
21
32
}
···
1
1
+
'use client';
2
2
+
1
3
import {
2
4
ScrollArea,
3
5
Stack,
···
73
75
return <CollectionSelectorError />;
74
76
}
75
77
76
76
-
const hasCollections = data?.collections?.length > 0;
78
78
+
const allCollections =
79
79
+
data?.pages.flatMap((page) => page.collections ?? []) ?? [];
80
80
+
81
81
+
const hasCollections = allCollections.length > 0;
77
82
const hasSelectedCollections = props.selectedCollections.length > 0;
78
83
79
84
return (
···
117
122
</Tabs.Tab>
118
123
</Tabs.List>
119
124
120
120
-
{/* collections Panel */}
125
125
+
{/* Collections Panel */}
121
126
<Tabs.Panel value="collections" my="xs" w="100%">
122
127
<ScrollArea h={340} type="auto">
123
128
<Stack gap="xs">
···
126
131
<Button
127
132
variant="light"
128
133
size="md"
129
129
-
color={'grape'}
130
130
-
radius={'lg'}
134
134
+
color="grape"
135
135
+
radius="lg"
131
136
leftSection={<BiPlus size={22} />}
132
137
onClick={() => setIsDrawerOpen(true)}
133
138
>
···
156
161
))}
157
162
</Fragment>
158
163
) : hasCollections ? (
159
159
-
renderCollectionItems(data.collections)
164
164
+
renderCollectionItems(allCollections)
160
165
) : (
161
166
<Stack align="center" gap="xs">
162
167
<Text fz="lg" fw={600} c="gray">
···
176
181
</ScrollArea>
177
182
</Tabs.Panel>
178
183
179
179
-
{/* selected Collections Panel */}
184
184
+
{/* Selected Collections Panel */}
180
185
<Tabs.Panel value="selected" my="xs">
181
186
<ScrollArea h={340} type="auto">
182
187
<Stack gap="xs">
183
183
-
{props.selectedCollections.length > 0 ? (
188
188
+
{hasSelectedCollections ? (
184
189
renderCollectionItems(props.selectedCollections)
185
190
) : (
186
191
<Alert color="gray" title="No collections selected" />
···
189
194
</ScrollArea>
190
195
</Tabs.Panel>
191
196
</Tabs>
192
192
-
<Group justify="space-between" gap={'xs'} grow>
197
197
+
198
198
+
<Group justify="space-between" gap="xs" grow>
193
199
<Button
194
200
variant="light"
195
195
-
color={'gray'}
201
201
+
color="gray"
196
202
size="md"
197
203
onClick={() => {
198
204
props.onSelectedCollectionsChange([]);
···
218
224
</Stack>
219
225
</Container>
220
226
</Drawer>
227
227
+
221
228
<CreateCollectionDrawer
222
229
key={search}
223
230
isOpen={isDrawerOpen}
···
8
8
import CreateCollectionShortcut from '../createCollectionShortcut/CreateCollectionShortcut';
9
9
10
10
export default function CollectionsNavList() {
11
11
-
const { data, error } = useCollections();
11
11
+
const { data, error } = useCollections({ limit: 30 });
12
12
const [opened, toggleMenu] = useToggle([true, false]);
13
13
14
14
if (error) {
15
15
return <CollectionsNavListError />;
16
16
}
17
17
+
18
18
+
const collections =
19
19
+
data?.pages.flatMap((page) => page.collections ?? []) ?? [];
17
20
18
21
return (
19
22
<NavLink
···
24
27
opened={opened}
25
28
onClick={() => toggleMenu()}
26
29
>
27
27
-
{/* Self-contained shortcut to prevent won't re-render this whole list when interacted with */}
28
30
<CreateCollectionShortcut />
29
31
30
32
<NavLink
31
33
component={Link}
32
34
href="/collections"
33
33
-
label={'View all'}
35
35
+
label="View all"
34
36
variant="subtle"
35
37
c="blue"
36
38
leftSection={<BiRightArrowAlt size={25} />}
37
39
/>
38
40
39
41
<Stack gap={0}>
40
40
-
{data.collections.map((c) => (
42
42
+
{collections.map((collection) => (
41
43
<CollectionNavItem
42
42
-
key={c.id}
43
43
-
name={c.name}
44
44
-
url={`/collections/${c.id}`}
45
45
-
cardCount={c.cardCount}
44
44
+
key={collection.id}
45
45
+
name={collection.name}
46
46
+
url={`/collections/${collection.id}`}
47
47
+
cardCount={collection.cardCount}
46
48
/>
47
49
))}
48
50
</Stack>
···
1
1
'use client';
2
2
3
3
import {
4
4
-
ActionIcon,
5
4
Anchor,
6
5
Box,
7
6
Button,
8
7
Container,
9
8
Grid,
10
9
Group,
11
11
-
Menu,
12
10
Stack,
13
11
Text,
14
12
Title,
13
13
+
Center,
14
14
+
Loader,
15
15
} from '@mantine/core';
16
16
import useCollection from '../../lib/queries/useCollection';
17
17
import UrlCard from '@/features/cards/components/urlCard/UrlCard';
18
18
-
import { BsTrash2Fill, BsThreeDots, BsPencilFill } from 'react-icons/bs';
19
18
import Link from 'next/link';
20
19
import { useState } from 'react';
21
21
-
import EditCollectionDrawer from '../../components/editCollectionDrawer/EditCollectionDrawer';
22
20
import { BiPlus } from 'react-icons/bi';
23
23
-
import DeleteCollectionModal from '../../components/deleteCollectionModal/DeleteCollectionModal';
24
21
import AddCardDrawer from '@/features/cards/components/addCardDrawer/AddCardDrawer';
25
22
import CollectionActions from '../../components/collectionActions/CollectionActions';
23
23
+
import CollectionContainerError from './Error.CollectionContainer';
24
24
+
import CollectionContainerSkeleton from './Skeleton.CollectionContainer';
26
25
27
26
interface Props {
28
27
id: string;
29
28
}
30
29
31
31
-
export default function CollectionContainer(props: Props) {
32
32
-
const { data } = useCollection({ id: props.id });
30
30
+
export default function CollectionContainer({ id }: Props) {
31
31
+
const {
32
32
+
data,
33
33
+
isPending,
34
34
+
error,
35
35
+
fetchNextPage,
36
36
+
hasNextPage,
37
37
+
isFetchingNextPage,
38
38
+
} = useCollection({ id });
39
39
+
33
40
const [showAddDrawer, setShowAddDrawer] = useState(false);
41
41
+
42
42
+
if (isPending) {
43
43
+
return <CollectionContainerSkeleton />;
44
44
+
}
45
45
+
46
46
+
if (error) {
47
47
+
return <CollectionContainerError />;
48
48
+
}
49
49
+
50
50
+
const firstPage = data.pages[0];
51
51
+
const allCards = data.pages.flatMap((page) => page.urlCards ?? []);
34
52
35
53
return (
36
36
-
<Container p={'xs'} size={'xl'}>
54
54
+
<Container p="xs" size="xl">
37
55
<Stack justify="flex-start">
38
56
<Group justify="space-between" align="start">
39
57
<Stack gap={0}>
40
40
-
<Text fw={700} c={'grape'}>
58
58
+
<Text fw={700} c="grape">
41
59
Collection
42
60
</Text>
43
61
<Title order={1} lh={0.8}>
44
44
-
{data.name}
62
62
+
{firstPage.name}
45
63
</Title>
46
46
-
{data.description && (
47
47
-
<Text c={'gray'} mt={'lg'}>
48
48
-
{data.description}
64
64
+
{firstPage.description && (
65
65
+
<Text c="gray" mt="lg">
66
66
+
{firstPage.description}
49
67
</Text>
50
68
)}
51
69
</Stack>
52
70
53
71
<Stack>
54
54
-
<Text fw={600} c={'gray.7'}>
72
72
+
<Text fw={600} c="gray.7">
55
73
By{' '}
56
74
<Anchor
57
75
component={Link}
58
58
-
href={`/profile/${data.author.handle}`}
76
76
+
href={`/profile/${firstPage.author.handle}`}
59
77
fw={700}
60
60
-
c={'blue'}
78
78
+
c="blue"
61
79
>
62
62
-
{data.author.name}
80
80
+
{firstPage.author.name}
63
81
</Anchor>
64
82
</Text>
65
83
</Stack>
···
67
85
68
86
<Group justify="end">
69
87
<CollectionActions
70
70
-
id={props.id}
71
71
-
name={data.name}
72
72
-
description={data.description}
73
73
-
authorHandle={data.author.handle}
88
88
+
id={id}
89
89
+
name={firstPage.name}
90
90
+
description={firstPage.description}
91
91
+
authorHandle={firstPage.author.handle}
74
92
/>
75
93
</Group>
76
94
77
77
-
{data.urlCards.length > 0 ? (
78
78
-
<Grid gutter={'md'}>
79
79
-
{data.urlCards.map((card) => (
80
80
-
<Grid.Col key={card.id} span={{ base: 12, xs: 6, sm: 4, lg: 3 }}>
81
81
-
<UrlCard
82
82
-
id={card.id}
83
83
-
url={card.url}
84
84
-
cardContent={card.cardContent}
85
85
-
note={card.note}
86
86
-
currentCollection={{
87
87
-
id: data.id,
88
88
-
name: data.name,
89
89
-
authorId: data.author.id,
90
90
-
}}
91
91
-
/>
92
92
-
</Grid.Col>
93
93
-
))}
94
94
-
</Grid>
95
95
+
{allCards.length > 0 ? (
96
96
+
<>
97
97
+
<Grid gutter="md">
98
98
+
{allCards.map((card) => (
99
99
+
<Grid.Col
100
100
+
key={card.id}
101
101
+
span={{ base: 12, xs: 6, sm: 4, lg: 3 }}
102
102
+
>
103
103
+
<UrlCard
104
104
+
id={card.id}
105
105
+
url={card.url}
106
106
+
cardContent={card.cardContent}
107
107
+
note={card.note}
108
108
+
currentCollection={{
109
109
+
id: firstPage.id,
110
110
+
name: firstPage.name,
111
111
+
authorId: firstPage.author.id,
112
112
+
}}
113
113
+
/>
114
114
+
</Grid.Col>
115
115
+
))}
116
116
+
</Grid>
117
117
+
118
118
+
{hasNextPage && (
119
119
+
<Center>
120
120
+
<Button
121
121
+
mt="md"
122
122
+
variant="light"
123
123
+
color="gray"
124
124
+
onClick={() => fetchNextPage()}
125
125
+
loading={isFetchingNextPage}
126
126
+
>
127
127
+
{isFetchingNextPage ? 'Loading more...' : 'Load More'}
128
128
+
</Button>
129
129
+
</Center>
130
130
+
)}
131
131
+
</>
95
132
) : (
96
96
-
<Stack align="center" gap={'xs'}>
97
97
-
<Text fz={'h3'} fw={600} c={'gray'}>
133
133
+
<Stack align="center" gap="xs">
134
134
+
<Text fz="h3" fw={600} c="gray">
98
135
No cards
99
136
</Text>
100
137
<Button
101
138
variant="light"
102
102
-
color={'gray'}
139
139
+
color="gray"
103
140
size="md"
104
141
rightSection={<BiPlus size={22} />}
105
142
onClick={() => setShowAddDrawer(true)}
···
115
152
isOpen={showAddDrawer}
116
153
onClose={() => setShowAddDrawer(false)}
117
154
selectedCollection={{
118
118
-
id: data.id,
119
119
-
name: data.name,
120
120
-
cardCount: data.urlCards.length,
155
155
+
id: firstPage.id,
156
156
+
name: firstPage.name,
157
157
+
cardCount: allCards.length,
121
158
}}
122
159
/>
123
160
</Box>
···
7
7
Title,
8
8
Text,
9
9
SimpleGrid,
10
10
+
Center,
10
11
} from '@mantine/core';
11
12
import useCollections from '../../lib/queries/useCollections';
12
13
import { BiPlus } from 'react-icons/bi';
···
15
16
import CreateCollectionDrawer from '../../components/createCollectionDrawer/CreateCollectionDrawer';
16
17
17
18
export default function CollectionsContainer() {
18
18
-
const { data } = useCollections();
19
19
+
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
20
20
+
useCollections();
21
21
+
19
22
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
20
23
24
24
+
const collections =
25
25
+
data?.pages.flatMap((page) => page.collections ?? []) ?? [];
26
26
+
21
27
return (
22
22
-
<Container p={'xs'} size={'xl'}>
28
28
+
<Container p="xs" size="xl">
23
29
<Stack>
24
30
<Title order={1}>Collections</Title>
25
31
26
26
-
{data.collections.length > 0 ? (
27
27
-
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing={'md'}>
28
28
-
{data.collections.map((c) => (
29
29
-
<CollectionCard key={c.id} collection={c} />
30
30
-
))}
31
31
-
</SimpleGrid>
32
32
+
{collections.length > 0 ? (
33
33
+
<Stack>
34
34
+
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="md">
35
35
+
{collections.map((collection) => (
36
36
+
<CollectionCard key={collection.id} collection={collection} />
37
37
+
))}
38
38
+
</SimpleGrid>
39
39
+
40
40
+
{hasNextPage && (
41
41
+
<Center>
42
42
+
<Button
43
43
+
onClick={() => fetchNextPage()}
44
44
+
disabled={isFetchingNextPage}
45
45
+
loading={isFetchingNextPage}
46
46
+
variant="light"
47
47
+
color="gray"
48
48
+
mt="md"
49
49
+
>
50
50
+
Load More
51
51
+
</Button>
52
52
+
</Center>
53
53
+
)}
54
54
+
</Stack>
32
55
) : (
33
33
-
<Stack align="center" gap={'xs'}>
34
34
-
<Text fz={'h3'} fw={600} c={'gray'}>
56
56
+
<Stack align="center" gap="xs">
57
57
+
<Text fz="h3" fw={600} c="gray">
35
58
No collections
36
59
</Text>
37
60
<Button
38
61
onClick={() => setIsDrawerOpen(true)}
39
62
variant="light"
40
40
-
color={'gray'}
63
63
+
color="gray"
41
64
size="md"
42
65
rightSection={<BiPlus size={22} />}
43
66
>
44
67
Create your first collection
45
68
</Button>
46
46
-
<CreateCollectionDrawer
47
47
-
isOpen={isDrawerOpen}
48
48
-
onClose={() => setIsDrawerOpen(false)}
49
49
-
/>
50
69
</Stack>
51
70
)}
52
71
</Stack>
72
72
+
73
73
+
<CreateCollectionDrawer
74
74
+
isOpen={isDrawerOpen}
75
75
+
onClose={() => setIsDrawerOpen(false)}
76
76
+
/>
53
77
</Container>
54
78
);
55
79
}
···
1
1
import { ApiClient } from '@/api-client/ApiClient';
2
2
import { getAccessToken } from '@/services/auth';
3
3
-
import { useSuspenseQuery } from '@tanstack/react-query';
3
3
+
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
4
4
5
5
interface Props {
6
6
id: string;
7
7
+
limit?: number;
7
8
}
8
9
9
10
export default function useCollection(props: Props) {
···
12
13
() => getAccessToken(),
13
14
);
14
15
15
15
-
// TODO: replace with infinite suspense query
16
16
-
const collection = useSuspenseQuery({
17
17
-
queryKey: ['collection', props.id],
18
18
-
queryFn: () =>
19
19
-
apiClient.getCollectionPage(props.id, {
20
20
-
limit: 50,
21
21
-
}),
22
22
-
});
16
16
+
const limit = props.limit ?? 20;
23
17
24
24
-
return collection;
18
18
+
return useSuspenseInfiniteQuery({
19
19
+
queryKey: ['collection', props.id, limit],
20
20
+
initialPageParam: 1,
21
21
+
queryFn: ({ pageParam }) =>
22
22
+
apiClient.getCollectionPage(props.id, { limit, page: pageParam }),
23
23
+
getNextPageParam: (lastPage) => {
24
24
+
return lastPage.pagination.hasMore
25
25
+
? lastPage.pagination.currentPage + 1
26
26
+
: undefined;
27
27
+
},
28
28
+
});
25
29
}
···
1
1
import { ApiClient } from '@/api-client/ApiClient';
2
2
import { getAccessToken } from '@/services/auth';
3
3
-
import { useSuspenseQuery } from '@tanstack/react-query';
3
3
+
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
4
4
5
5
interface Props {
6
6
limit?: number;
···
12
12
() => getAccessToken(),
13
13
);
14
14
15
15
-
const collections = useSuspenseQuery({
16
16
-
queryKey: ['collections', props?.limit],
17
17
-
queryFn: () => apiClient.getMyCollections({ limit: props?.limit ?? 50 }),
15
15
+
const limit = props?.limit ?? 15;
16
16
+
17
17
+
return useSuspenseInfiniteQuery({
18
18
+
queryKey: ['collections', limit],
19
19
+
initialPageParam: 1,
20
20
+
queryFn: ({ pageParam }) =>
21
21
+
apiClient.getMyCollections({ limit, page: pageParam }),
22
22
+
getNextPageParam: (lastPage) => {
23
23
+
return lastPage.pagination.hasMore
24
24
+
? lastPage.pagination.currentPage + 1
25
25
+
: undefined;
26
26
+
},
18
27
});
19
19
-
20
20
-
return collections;
21
28
}
···
1
1
+
import { Alert } from '@mantine/core';
2
2
+
3
3
+
export default function MyFeedContainerError() {
4
4
+
return <Alert variant="white" color="red" title="Could not load feed" />;
5
5
+
}
···
1
1
+
'use client';
2
2
+
3
3
+
import useMyFeed from '@/features/feeds/lib/queries/useMyFeed';
4
4
+
import FeedItem from '@/features/feeds/components/feedItem/FeedItem';
5
5
+
import { Button, Stack, Title, Text, Center, Container } from '@mantine/core';
6
6
+
import MyFeedContainerSkeleton from './Skeleton.MyFeedContainer';
7
7
+
import MyFeedContainerError from './Error.MyFeedContainer';
8
8
+
9
9
+
export default function MyFeedContainer() {
10
10
+
const {
11
11
+
data,
12
12
+
error,
13
13
+
isPending,
14
14
+
fetchNextPage,
15
15
+
hasNextPage,
16
16
+
isFetchingNextPage,
17
17
+
} = useMyFeed();
18
18
+
19
19
+
const allActivities =
20
20
+
data?.pages.flatMap((page) => page.activities ?? []) ?? [];
21
21
+
22
22
+
if (isPending) {
23
23
+
return <MyFeedContainerSkeleton />;
24
24
+
}
25
25
+
26
26
+
if (error) {
27
27
+
return <MyFeedContainerError />;
28
28
+
}
29
29
+
30
30
+
return (
31
31
+
<Container p="xs" size="xl">
32
32
+
<Stack>
33
33
+
<Title order={2}>Explore</Title>
34
34
+
35
35
+
{allActivities.length === 0 ? (
36
36
+
<Center h={200}>
37
37
+
<Text c="dimmed">No activity to show yet</Text>
38
38
+
</Center>
39
39
+
) : (
40
40
+
<Stack gap="xl">
41
41
+
{allActivities.map((item) => (
42
42
+
<FeedItem key={item.id} item={item} />
43
43
+
))}
44
44
+
45
45
+
{hasNextPage && (
46
46
+
<Center>
47
47
+
<Button
48
48
+
onClick={() => fetchNextPage()}
49
49
+
loading={isFetchingNextPage}
50
50
+
variant="light"
51
51
+
color="gray"
52
52
+
>
53
53
+
Load More
54
54
+
</Button>
55
55
+
</Center>
56
56
+
)}
57
57
+
</Stack>
58
58
+
)}
59
59
+
</Stack>
60
60
+
</Container>
61
61
+
);
62
62
+
}
···
1
1
+
import { Loader } from '@mantine/core';
2
2
+
3
3
+
export default function MyFeedContainerSkeleton() {
4
4
+
return <Loader />;
5
5
+
}
···
1
1
+
import { ApiClient } from '@/api-client/ApiClient';
2
2
+
import { getAccessToken } from '@/services/auth';
3
3
+
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
4
4
+
5
5
+
interface Props {
6
6
+
limit?: number;
7
7
+
}
8
8
+
9
9
+
export default function useMyFeed(props?: Props) {
10
10
+
const apiClient = new ApiClient(
11
11
+
process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000',
12
12
+
() => getAccessToken(),
13
13
+
);
14
14
+
15
15
+
const limit = props?.limit ?? 15;
16
16
+
17
17
+
const query = useSuspenseInfiniteQuery({
18
18
+
queryKey: ['my feed', limit],
19
19
+
initialPageParam: 1,
20
20
+
queryFn: ({ pageParam = 1 }) => {
21
21
+
return apiClient.getGlobalFeed({ limit, page: pageParam });
22
22
+
},
23
23
+
getNextPageParam: (lastPage) => {
24
24
+
if (lastPage.pagination.hasMore) {
25
25
+
return lastPage.pagination.currentPage + 1;
26
26
+
}
27
27
+
return undefined;
28
28
+
},
29
29
+
});
30
30
+
31
31
+
return query;
32
32
+
}
···
23
23
import AddCardDrawer from '@/features/cards/components/addCardDrawer/AddCardDrawer';
24
24
25
25
export default function LibraryContainer() {
26
26
-
const { data: CollectionsData } = useCollections({ limit: 4 });
26
26
+
const { data: collectionsData } = useCollections({ limit: 4 });
27
27
const { data: myCardsData } = useMyCards({ limit: 4 });
28
28
+
28
29
const [showCollectionDrawer, setShowCollectionDrawer] = useState(false);
29
30
const [showAddDrawer, setShowAddDrawer] = useState(false);
30
31
32
32
+
const collections =
33
33
+
collectionsData?.pages.flatMap((page) => page.collections) ?? [];
34
34
+
const cards = myCardsData?.pages.flatMap((page) => page.cards) ?? [];
35
35
+
31
36
return (
32
32
-
<Container p={'xs'} size={'xl'}>
33
33
-
<Stack gap={'xl'}>
37
37
+
<Container p="xs" size="xl">
38
38
+
<Stack gap="xl">
34
39
<Title order={1}>Library</Title>
35
40
36
41
<Stack gap={50}>
42
42
+
{/* Collections */}
37
43
<Stack>
38
44
<Group justify="space-between">
39
39
-
<Group gap={'xs'}>
45
45
+
<Group gap="xs">
40
46
<BiCollection size={22} />
41
47
<Title order={2}>Collections</Title>
42
48
</Group>
43
43
-
<Anchor component={Link} href={'/collections'} c="blue" fw={600}>
49
49
+
<Anchor component={Link} href="/collections" c="blue" fw={600}>
44
50
View all
45
51
</Anchor>
46
52
</Group>
47
53
48
48
-
{CollectionsData.collections.length > 0 ? (
49
49
-
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing={'md'}>
50
50
-
{CollectionsData.collections.map((c) => (
51
51
-
<CollectionCard key={c.id} collection={c} />
54
54
+
{collections.length > 0 ? (
55
55
+
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="md">
56
56
+
{collections.map((collection) => (
57
57
+
<CollectionCard key={collection.id} collection={collection} />
52
58
))}
53
59
</SimpleGrid>
54
60
) : (
55
55
-
<Stack align="center" gap={'xs'}>
56
56
-
<Text fz={'h3'} fw={600} c={'gray'}>
61
61
+
<Stack align="center" gap="xs">
62
62
+
<Text fz="h3" fw={600} c="gray">
57
63
No collections
58
64
</Text>
59
65
<Button
60
66
onClick={() => setShowCollectionDrawer(true)}
61
67
variant="light"
62
62
-
color={'gray'}
68
68
+
color="gray"
63
69
size="md"
64
70
rightSection={<BiPlus size={22} />}
65
71
>
···
69
75
)}
70
76
</Stack>
71
77
78
78
+
{/* Cards */}
72
79
<Stack>
73
80
<Group justify="space-between">
74
74
-
<Group gap={'xs'}>
81
81
+
<Group gap="xs">
75
82
<FaRegNoteSticky size={22} />
76
83
<Title order={2}>My Cards</Title>
77
84
</Group>
78
78
-
<Anchor component={Link} href={'/my-cards'} c="blue" fw={600}>
85
85
+
<Anchor component={Link} href="/my-cards" c="blue" fw={600}>
79
86
View all
80
87
</Anchor>
81
88
</Group>
82
82
-
{myCardsData.cards.length > 0 ? (
83
83
-
<Grid gutter={'md'}>
84
84
-
{myCardsData.cards.map((card) => (
89
89
+
90
90
+
{cards.length > 0 ? (
91
91
+
<Grid gutter="md">
92
92
+
{cards.map((card) => (
85
93
<Grid.Col
86
94
key={card.id}
87
95
span={{ base: 12, xs: 6, sm: 4, lg: 3 }}
···
97
105
))}
98
106
</Grid>
99
107
) : (
100
100
-
<Stack align="center" gap={'xs'}>
101
101
-
<Text fz={'h3'} fw={600} c={'gray'}>
108
108
+
<Stack align="center" gap="xs">
109
109
+
<Text fz="h3" fw={600} c="gray">
102
110
No cards
103
111
</Text>
104
112
<Button
105
113
variant="light"
106
106
-
color={'gray'}
114
114
+
color="gray"
107
115
size="md"
108
116
rightSection={<BiPlus size={22} />}
109
117
onClick={() => setShowAddDrawer(true)}
110
118
>
111
119
Add your first card
112
120
</Button>
113
113
-
<AddCardDrawer
114
114
-
isOpen={showAddDrawer}
115
115
-
onClose={() => setShowAddDrawer(false)}
116
116
-
/>
117
121
</Stack>
118
122
)}
119
123
</Stack>
120
124
</Stack>
121
125
</Stack>
126
126
+
127
127
+
{/* Drawers */}
122
128
<CreateCollectionDrawer
123
129
isOpen={showCollectionDrawer}
124
130
onClose={() => setShowCollectionDrawer(false)}
131
131
+
/>
132
132
+
<AddCardDrawer
133
133
+
isOpen={showAddDrawer}
134
134
+
onClose={() => setShowAddDrawer(false)}
125
135
/>
126
136
</Container>
127
137
);