tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
28
pulls
pipelines
added profile settings to nav
cozylittle.house
1 month ago
c2d45121
93d2ec1d
+95
-614
14 changed files
expand all
collapse all
unified
split
app
(home-pages)
discover
SortButtons.tsx
SortedPublicationList.tsx
getPublications.ts
page.tsx
p
[didOrHandle]
ProfileLayout.tsx
PubListing.tsx
comments
CommentsContent.tsx
subscriptions
SubscriptionsContent.tsx
reader
SubscriptionsContent.tsx
getSubscriptions.ts
lish
Subscribe.tsx
components
ActionBar
ProfileButton.tsx
Icons
RSSTiny.tsx
Popover
index.tsx
+18
-12
app/(home-pages)/discover/PubListing.tsx
app/(home-pages)/p/[didOrHandle]/PubListing.tsx
···
1
1
"use client";
2
2
import { AtUri } from "@atproto/syntax";
3
3
import { PublicationSubscription } from "app/(home-pages)/reader/getSubscriptions";
4
4
-
import { SubscribeWithBluesky } from "app/lish/Subscribe";
4
4
+
import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
5
5
+
import { ManageSubscription, SubscribeWithBluesky } from "app/lish/Subscribe";
5
6
import { PubIcon } from "components/ActionBar/Publications";
6
7
import { Separator } from "components/Layout";
7
8
import { usePubTheme } from "components/ThemeManager/PublicationThemeProvider";
···
9
10
import { blobRefToSrc } from "src/utils/blobRefToSrc";
10
11
import { timeAgo } from "src/utils/timeAgo";
11
12
12
12
-
export const PubListing = (
13
13
-
props: PublicationSubscription & {
14
14
-
resizeHeight?: boolean;
15
15
-
},
16
16
-
) => {
13
13
+
export const PubListing = (props: PublicationSubscription) => {
17
14
let record = props.record;
18
15
let theme = usePubTheme(record?.theme);
19
16
let backgroundImage = record?.theme?.backgroundImage?.image?.ref
···
28
25
if (!record) return null;
29
26
return (
30
27
<BaseThemeProvider {...theme} local>
31
31
-
<a
32
32
-
href={record.url}
28
28
+
<div
33
29
className={`no-underline! flex flex-row gap-2
34
30
bg-bg-leaflet
35
31
border border-border-light rounded-lg
···
42
38
backgroundSize: `${backgroundImageRepeat ? `${backgroundImageSize}px` : "cover"}`,
43
39
}}
44
40
>
41
41
+
<a href={record.url} className="absolute inset-0" />
45
42
<div
46
46
-
className={`flex w-full flex-col justify-center text-center max-h-48 pt-4 pb-3 px-3 rounded-lg relative z-10 ${props.resizeHeight ? "" : "sm:h-48 h-full"} ${record.theme?.showPageBackground ? "bg-[rgba(var(--bg-page),var(--bg-page-alpha))] " : ""}`}
43
43
+
className={`flex w-full flex-col justify-center text-center pt-4 pb-3 px-3 rounded-lg relative z-10 sm:h-[200px] h-full ${record.theme?.showPageBackground ? "bg-[rgba(var(--bg-page),var(--bg-page-alpha))] " : ""}`}
47
44
>
48
45
<div className="mx-auto pb-1">
49
46
<PubIcon record={record} uri={props.uri} large />
···
51
48
52
49
<h4 className="truncate shrink-0 ">{record.name}</h4>
53
50
{record.description && (
54
54
-
<p className="text-secondary text-sm max-h-full overflow-hidden pb-1">
51
51
+
<p className="text-secondary line-clamp-1 min-h-[16px] text-sm overflow-hidden ">
55
52
{record.description}
56
53
</p>
57
54
)}
58
58
-
<div className="flex flex-col items-center justify-center text-xs text-tertiary pt-2">
55
55
+
<div className="flex flex-col items-center justify-center text-xs text-tertiary pt-1">
59
56
<div className="flex flex-row gap-2 items-center">
60
57
{props.authorProfile?.handle}
61
58
</div>
···
67
64
)}
68
65
</p>
69
66
</div>
67
67
+
<div className="w-fit mx-auto mt-3 grow items-end flex">
68
68
+
<SubscribeWithBluesky
69
69
+
compact
70
70
+
pub_uri={props.uri}
71
71
+
pubName={props.record.name}
72
72
+
subscribers={props.publication_subscriptions || []}
73
73
+
base_url={getPublicationURL({ ...props })}
74
74
+
/>
75
75
+
</div>
70
76
</div>
71
71
-
</a>
77
77
+
</div>
72
78
</BaseThemeProvider>
73
79
);
74
80
};
-97
app/(home-pages)/discover/SortButtons.tsx
···
1
1
-
"use client";
2
2
-
import Link from "next/link";
3
3
-
import { useState } from "react";
4
4
-
import { theme } from "tailwind.config";
5
5
-
6
6
-
export default function SortButtons(props: { order: string }) {
7
7
-
const [selected, setSelected] = useState<"recentlyUpdated" | "popular">(
8
8
-
"recentlyUpdated",
9
9
-
);
10
10
-
11
11
-
return (
12
12
-
<div className="flex gap-2 pt-1">
13
13
-
<Link href="?order=recentlyUpdated">
14
14
-
<SortButton selected={props.order === "recentlyUpdated"}>
15
15
-
Recently Updated
16
16
-
</SortButton>
17
17
-
</Link>
18
18
-
19
19
-
<Link href="?order=popular">
20
20
-
<SortButton selected={props.order === "popular"}>Popular</SortButton>
21
21
-
</Link>
22
22
-
</div>
23
23
-
);
24
24
-
}
25
25
-
26
26
-
const SortButton = (props: {
27
27
-
children: React.ReactNode;
28
28
-
selected: boolean;
29
29
-
}) => {
30
30
-
return (
31
31
-
<div className="relative">
32
32
-
<button
33
33
-
style={
34
34
-
props.selected
35
35
-
? { backgroundColor: `rgba(var(--accent-1), 0.2)` }
36
36
-
: {}
37
37
-
}
38
38
-
className={`text-sm rounded-md px-[8px] py-0.5 border ${props.selected ? "border-accent-contrast text-accent-1 font-bold" : "text-tertiary border-border-light"}`}
39
39
-
>
40
40
-
{props.children}
41
41
-
</button>
42
42
-
{props.selected && (
43
43
-
<>
44
44
-
<div className="absolute top-0 -left-2">
45
45
-
<GlitterBig />
46
46
-
</div>
47
47
-
<div className="absolute top-4 left-0">
48
48
-
<GlitterSmall />
49
49
-
</div>
50
50
-
<div className="absolute -top-2 -right-1">
51
51
-
<GlitterSmall />
52
52
-
</div>
53
53
-
</>
54
54
-
)}
55
55
-
</div>
56
56
-
);
57
57
-
};
58
58
-
59
59
-
const GlitterBig = () => {
60
60
-
return (
61
61
-
<svg
62
62
-
width="16"
63
63
-
height="17"
64
64
-
viewBox="0 0 16 17"
65
65
-
fill="none"
66
66
-
xmlns="http://www.w3.org/2000/svg"
67
67
-
>
68
68
-
<path
69
69
-
d="M8.16553 0.804321C8.5961 0.804329 8.97528 1.03925 9.22803 1.40393C9.47845 1.76546 9.6128 2.25816 9.61279 2.84338C9.61279 2.98187 9.6178 3.11647 9.62646 3.2467C9.65365 3.65499 9.72104 4.02319 9.81006 4.35022C10.0833 5.35388 10.5641 5.96726 10.7349 6.14221C10.7443 6.15184 10.7543 6.16234 10.7642 6.17249C10.9808 6.39533 11.3925 6.8162 12.0142 7.09338C12.206 7.17892 12.4177 7.2502 12.6489 7.29749C12.8402 7.3366 13.0466 7.35993 13.2681 7.35999H13.269C14.2688 7.36032 14.9747 7.96603 14.9771 8.77014C14.9793 9.57755 14.272 10.1833 13.2681 10.1832C13.0278 10.1832 12.8137 10.2034 12.6226 10.2369C12.3793 10.2796 12.1697 10.3455 11.9858 10.4254C11.4714 10.6492 11.1325 10.9918 10.7935 11.3405C10.7739 11.3605 10.7544 11.381 10.7349 11.401C10.3936 11.7507 10.0271 12.1792 9.81006 12.9352C9.72175 13.2428 9.65679 13.6119 9.63135 14.0592C9.62378 14.1924 9.61963 14.3325 9.61963 14.4801C9.61963 15.5836 9.06909 16.4876 8.17822 16.4996C7.74928 16.5053 7.36767 16.2783 7.11182 15.9147C6.85918 15.5556 6.72412 15.065 6.72412 14.4801C6.72412 14.3385 6.71808 14.2015 6.70654 14.069C6.6724 13.6774 6.59177 13.324 6.48779 13.0123C6.16402 12.0419 5.61395 11.4722 5.54443 11.401C5.54371 11.4003 5.54043 11.3977 5.53467 11.3922C5.52778 11.3857 5.51839 11.3767 5.50635 11.3658C5.4823 11.3442 5.44954 11.3158 5.40869 11.2819C5.3268 11.2139 5.21473 11.1255 5.07764 11.0289C4.80173 10.8346 4.43374 10.6113 4.01611 10.443C3.82579 10.3663 3.62728 10.3019 3.42432 10.2565C3.21687 10.21 3.00599 10.1832 2.79541 10.1832C1.79834 10.1832 1.11533 9.56575 1.11865 8.76917C1.12219 7.9773 1.80451 7.36002 2.79541 7.35999C3.01821 7.35999 3.22798 7.33422 3.42432 7.29065C3.62557 7.24597 3.81426 7.18216 3.98877 7.10608C4.6567 6.81484 5.10772 6.35442 5.3042 6.15295C5.30777 6.1493 5.31147 6.14577 5.31494 6.14221C5.51076 5.94157 6.14024 5.28964 6.48584 4.26233C6.59001 3.95264 6.66793 3.60887 6.70068 3.23303C6.71166 3.10697 6.71826 2.977 6.71826 2.84338L6.72412 2.62854C6.75331 2.13723 6.88387 1.72031 7.10303 1.40393C7.35578 1.03923 7.73495 0.804326 8.16553 0.804321Z"
70
70
-
fill={theme.colors["accent-1"]}
71
71
-
stroke={theme.colors["bg-leaflet"]}
72
72
-
strokeLinecap="round"
73
73
-
strokeLinejoin="round"
74
74
-
/>
75
75
-
</svg>
76
76
-
);
77
77
-
};
78
78
-
79
79
-
const GlitterSmall = () => {
80
80
-
return (
81
81
-
<svg
82
82
-
width="13"
83
83
-
height="14"
84
84
-
viewBox="0 0 13 14"
85
85
-
fill="none"
86
86
-
xmlns="http://www.w3.org/2000/svg"
87
87
-
>
88
88
-
<path
89
89
-
d="M6.37585 1.23596C6.7489 1.23598 7.07064 1.44034 7.28015 1.7428C7.48716 2.04187 7.59266 2.4408 7.59265 2.901C7.59266 3.00294 7.59605 3.10213 7.60242 3.19788C7.62244 3.49844 7.67183 3.76938 7.73718 4.0094C7.93813 4.74731 8.29123 5.1934 8.4071 5.31213L8.57703 5.48206C8.75042 5.64731 9.00188 5.85577 9.33777 6.00549C9.47565 6.06695 9.62723 6.11812 9.79285 6.15198C9.92991 6.18 10.0779 6.1959 10.2372 6.19592C11.0418 6.19604 11.6503 6.69195 11.6522 7.38538C11.654 8.08176 11.0444 8.57683 10.2372 8.57678C10.0618 8.57678 9.90679 8.59077 9.76941 8.61487C9.59484 8.6455 9.4456 8.69297 9.31531 8.74963C8.95055 8.9083 8.70884 9.15057 8.45203 9.41467C8.43719 9.42993 8.42207 9.44621 8.4071 9.46155C8.15582 9.71904 7.89358 10.0262 7.73718 10.5709C7.67315 10.7941 7.62512 11.064 7.60632 11.3942C7.60073 11.4925 7.59754 11.5963 7.59753 11.7057C7.59753 12.5657 7.16303 13.3455 6.38757 13.3561C6.01608 13.3611 5.6911 13.1642 5.47839 12.8619C5.26902 12.5643 5.16394 12.1657 5.16394 11.7057C5.16393 11.6022 5.15871 11.5017 5.15027 11.4049C5.1253 11.1189 5.06701 10.861 4.99109 10.6334C4.75475 9.92518 4.35324 9.51044 4.30554 9.46155C4.30554 9.46155 4.27494 9.43195 4.21179 9.37952C4.15207 9.32993 4.06961 9.26511 3.96863 9.19397C3.76515 9.05064 3.49524 8.88718 3.19031 8.76428C3.05151 8.70835 2.90782 8.66129 2.7616 8.62854C2.61218 8.59509 2.4616 8.57679 2.31238 8.57678C1.50706 8.57678 0.918891 8.07006 0.921753 7.3844C0.924612 6.70329 1.51125 6.19594 2.31238 6.19592C2.47154 6.19591 2.6213 6.17821 2.7616 6.14709C2.90554 6.11514 3.04128 6.06904 3.16687 6.01428C3.64904 5.80395 3.97684 5.47074 4.1239 5.31995C4.12648 5.3173 4.12918 5.31473 4.13171 5.31213C4.27635 5.16393 4.73656 4.68608 4.98914 3.93518C5.06511 3.70931 5.12247 3.45905 5.14636 3.18518C5.15436 3.09338 5.15905 2.99838 5.15906 2.901C5.15906 2.44078 5.26453 2.04187 5.47156 1.7428C5.68108 1.44033 6.00279 1.23595 6.37585 1.23596Z"
90
90
-
fill={theme.colors["accent-1"]}
91
91
-
stroke={theme.colors["bg-leaflet"]}
92
92
-
strokeLinecap="round"
93
93
-
strokeLinejoin="round"
94
94
-
/>
95
95
-
</svg>
96
96
-
);
97
97
-
};
-195
app/(home-pages)/discover/SortedPublicationList.tsx
···
1
1
-
"use client";
2
2
-
import Link from "next/link";
3
3
-
import { useState, useEffect, useRef } from "react";
4
4
-
import { theme } from "tailwind.config";
5
5
-
import { PubListing } from "./PubListing";
6
6
-
import useSWRInfinite from "swr/infinite";
7
7
-
import { getPublications, type Cursor, type Publication } from "./getPublications";
8
8
-
9
9
-
export function SortedPublicationList(props: {
10
10
-
publications: Publication[];
11
11
-
order: string;
12
12
-
nextCursor: Cursor | null;
13
13
-
}) {
14
14
-
let [order, setOrder] = useState(props.order);
15
15
-
16
16
-
const getKey = (
17
17
-
pageIndex: number,
18
18
-
previousPageData: { publications: Publication[]; nextCursor: Cursor | null } | null,
19
19
-
) => {
20
20
-
// Reached the end
21
21
-
if (previousPageData && !previousPageData.nextCursor) return null;
22
22
-
23
23
-
// First page, we don't have previousPageData
24
24
-
if (pageIndex === 0) return ["discover-publications", order, null] as const;
25
25
-
26
26
-
// Add the cursor to the key
27
27
-
return ["discover-publications", order, previousPageData?.nextCursor] as const;
28
28
-
};
29
29
-
30
30
-
const { data, error, size, setSize, isValidating } = useSWRInfinite(
31
31
-
getKey,
32
32
-
([_, orderValue, cursor]) => {
33
33
-
const orderParam = orderValue === "popular" ? "popular" : "recentlyUpdated";
34
34
-
return getPublications(orderParam, cursor);
35
35
-
},
36
36
-
{
37
37
-
fallbackData: order === props.order
38
38
-
? [{ publications: props.publications, nextCursor: props.nextCursor }]
39
39
-
: undefined,
40
40
-
revalidateFirstPage: false,
41
41
-
},
42
42
-
);
43
43
-
44
44
-
const loadMoreRef = useRef<HTMLDivElement>(null);
45
45
-
46
46
-
// Set up intersection observer to load more when trigger element is visible
47
47
-
useEffect(() => {
48
48
-
const observer = new IntersectionObserver(
49
49
-
(entries) => {
50
50
-
if (entries[0].isIntersecting && !isValidating) {
51
51
-
const hasMore = data && data[data.length - 1]?.nextCursor;
52
52
-
if (hasMore) {
53
53
-
setSize(size + 1);
54
54
-
}
55
55
-
}
56
56
-
},
57
57
-
{ threshold: 0.1 },
58
58
-
);
59
59
-
60
60
-
if (loadMoreRef.current) {
61
61
-
observer.observe(loadMoreRef.current);
62
62
-
}
63
63
-
64
64
-
return () => observer.disconnect();
65
65
-
}, [data, size, setSize, isValidating]);
66
66
-
67
67
-
const allPublications = data ? data.flatMap((page) => page.publications) : [];
68
68
-
69
69
-
return (
70
70
-
<div className="discoverHeader flex flex-col items-center ">
71
71
-
<SortButtons
72
72
-
order={order}
73
73
-
setOrder={(o) => {
74
74
-
const url = new URL(window.location.href);
75
75
-
url.searchParams.set("order", o);
76
76
-
window.history.pushState({}, "", url);
77
77
-
setOrder(o);
78
78
-
}}
79
79
-
/>
80
80
-
<div className="discoverPubList flex flex-col gap-3 pt-6 w-full relative">
81
81
-
{allPublications.map((pub) => (
82
82
-
<PubListing resizeHeight key={pub.uri} {...pub} />
83
83
-
))}
84
84
-
{/* Trigger element for loading more publications */}
85
85
-
<div
86
86
-
ref={loadMoreRef}
87
87
-
className="absolute bottom-96 left-0 w-full h-px pointer-events-none"
88
88
-
aria-hidden="true"
89
89
-
/>
90
90
-
{isValidating && (
91
91
-
<div className="text-center text-tertiary py-4">
92
92
-
Loading more publications...
93
93
-
</div>
94
94
-
)}
95
95
-
</div>
96
96
-
</div>
97
97
-
);
98
98
-
}
99
99
-
100
100
-
export default function SortButtons(props: {
101
101
-
order: string;
102
102
-
setOrder: (order: string) => void;
103
103
-
}) {
104
104
-
const [selected, setSelected] = useState<"recentlyUpdated" | "popular">(
105
105
-
"recentlyUpdated",
106
106
-
);
107
107
-
108
108
-
return (
109
109
-
<div className="flex gap-2 pt-1">
110
110
-
<SortButton
111
111
-
selected={props.order === "recentlyUpdated"}
112
112
-
onClick={() => props.setOrder("recentlyUpdated")}
113
113
-
>
114
114
-
Recently Updated
115
115
-
</SortButton>
116
116
-
117
117
-
<SortButton
118
118
-
selected={props.order === "popular"}
119
119
-
onClick={() => props.setOrder("popular")}
120
120
-
>
121
121
-
Popular
122
122
-
</SortButton>
123
123
-
</div>
124
124
-
);
125
125
-
}
126
126
-
127
127
-
const SortButton = (props: {
128
128
-
children: React.ReactNode;
129
129
-
onClick: () => void;
130
130
-
selected: boolean;
131
131
-
}) => {
132
132
-
return (
133
133
-
<div className="relative">
134
134
-
<button
135
135
-
onClick={props.onClick}
136
136
-
className={`text-sm rounded-md px-[8px] font-bold py-0.5 border ${props.selected ? "border-accent-contrast bg-accent-1 text-accent-2 " : "bg-bg-page text-accent-contrast border-accent-contrast"}`}
137
137
-
>
138
138
-
{props.children}
139
139
-
</button>
140
140
-
{props.selected && (
141
141
-
<>
142
142
-
<div className="absolute top-0 -left-2">
143
143
-
<GlitterBig />
144
144
-
</div>
145
145
-
<div className="absolute top-4 left-0">
146
146
-
<GlitterSmall />
147
147
-
</div>
148
148
-
<div className="absolute -top-2 -right-1">
149
149
-
<GlitterSmall />
150
150
-
</div>
151
151
-
</>
152
152
-
)}
153
153
-
</div>
154
154
-
);
155
155
-
};
156
156
-
157
157
-
const GlitterBig = () => {
158
158
-
return (
159
159
-
<svg
160
160
-
width="16"
161
161
-
height="17"
162
162
-
viewBox="0 0 16 17"
163
163
-
fill="none"
164
164
-
xmlns="http://www.w3.org/2000/svg"
165
165
-
>
166
166
-
<path
167
167
-
d="M8.16553 0.804321C8.5961 0.804329 8.97528 1.03925 9.22803 1.40393C9.47845 1.76546 9.6128 2.25816 9.61279 2.84338C9.61279 2.98187 9.6178 3.11647 9.62646 3.2467C9.65365 3.65499 9.72104 4.02319 9.81006 4.35022C10.0833 5.35388 10.5641 5.96726 10.7349 6.14221C10.7443 6.15184 10.7543 6.16234 10.7642 6.17249C10.9808 6.39533 11.3925 6.8162 12.0142 7.09338C12.206 7.17892 12.4177 7.2502 12.6489 7.29749C12.8402 7.3366 13.0466 7.35993 13.2681 7.35999H13.269C14.2688 7.36032 14.9747 7.96603 14.9771 8.77014C14.9793 9.57755 14.272 10.1833 13.2681 10.1832C13.0278 10.1832 12.8137 10.2034 12.6226 10.2369C12.3793 10.2796 12.1697 10.3455 11.9858 10.4254C11.4714 10.6492 11.1325 10.9918 10.7935 11.3405C10.7739 11.3605 10.7544 11.381 10.7349 11.401C10.3936 11.7507 10.0271 12.1792 9.81006 12.9352C9.72175 13.2428 9.65679 13.6119 9.63135 14.0592C9.62378 14.1924 9.61963 14.3325 9.61963 14.4801C9.61963 15.5836 9.06909 16.4876 8.17822 16.4996C7.74928 16.5053 7.36767 16.2783 7.11182 15.9147C6.85918 15.5556 6.72412 15.065 6.72412 14.4801C6.72412 14.3385 6.71808 14.2015 6.70654 14.069C6.6724 13.6774 6.59177 13.324 6.48779 13.0123C6.16402 12.0419 5.61395 11.4722 5.54443 11.401C5.54371 11.4003 5.54043 11.3977 5.53467 11.3922C5.52778 11.3857 5.51839 11.3767 5.50635 11.3658C5.4823 11.3442 5.44954 11.3158 5.40869 11.2819C5.3268 11.2139 5.21473 11.1255 5.07764 11.0289C4.80173 10.8346 4.43374 10.6113 4.01611 10.443C3.82579 10.3663 3.62728 10.3019 3.42432 10.2565C3.21687 10.21 3.00599 10.1832 2.79541 10.1832C1.79834 10.1832 1.11533 9.56575 1.11865 8.76917C1.12219 7.9773 1.80451 7.36002 2.79541 7.35999C3.01821 7.35999 3.22798 7.33422 3.42432 7.29065C3.62557 7.24597 3.81426 7.18216 3.98877 7.10608C4.6567 6.81484 5.10772 6.35442 5.3042 6.15295C5.30777 6.1493 5.31147 6.14577 5.31494 6.14221C5.51076 5.94157 6.14024 5.28964 6.48584 4.26233C6.59001 3.95264 6.66793 3.60887 6.70068 3.23303C6.71166 3.10697 6.71826 2.977 6.71826 2.84338L6.72412 2.62854C6.75331 2.13723 6.88387 1.72031 7.10303 1.40393C7.35578 1.03923 7.73495 0.804326 8.16553 0.804321Z"
168
168
-
fill={theme.colors["accent-1"]}
169
169
-
stroke={theme.colors["bg-leaflet"]}
170
170
-
strokeLinecap="round"
171
171
-
strokeLinejoin="round"
172
172
-
/>
173
173
-
</svg>
174
174
-
);
175
175
-
};
176
176
-
177
177
-
const GlitterSmall = () => {
178
178
-
return (
179
179
-
<svg
180
180
-
width="13"
181
181
-
height="14"
182
182
-
viewBox="0 0 13 14"
183
183
-
fill="none"
184
184
-
xmlns="http://www.w3.org/2000/svg"
185
185
-
>
186
186
-
<path
187
187
-
d="M6.37585 1.23596C6.7489 1.23598 7.07064 1.44034 7.28015 1.7428C7.48716 2.04187 7.59266 2.4408 7.59265 2.901C7.59266 3.00294 7.59605 3.10213 7.60242 3.19788C7.62244 3.49844 7.67183 3.76938 7.73718 4.0094C7.93813 4.74731 8.29123 5.1934 8.4071 5.31213L8.57703 5.48206C8.75042 5.64731 9.00188 5.85577 9.33777 6.00549C9.47565 6.06695 9.62723 6.11812 9.79285 6.15198C9.92991 6.18 10.0779 6.1959 10.2372 6.19592C11.0418 6.19604 11.6503 6.69195 11.6522 7.38538C11.654 8.08176 11.0444 8.57683 10.2372 8.57678C10.0618 8.57678 9.90679 8.59077 9.76941 8.61487C9.59484 8.6455 9.4456 8.69297 9.31531 8.74963C8.95055 8.9083 8.70884 9.15057 8.45203 9.41467C8.43719 9.42993 8.42207 9.44621 8.4071 9.46155C8.15582 9.71904 7.89358 10.0262 7.73718 10.5709C7.67315 10.7941 7.62512 11.064 7.60632 11.3942C7.60073 11.4925 7.59754 11.5963 7.59753 11.7057C7.59753 12.5657 7.16303 13.3455 6.38757 13.3561C6.01608 13.3611 5.6911 13.1642 5.47839 12.8619C5.26902 12.5643 5.16394 12.1657 5.16394 11.7057C5.16393 11.6022 5.15871 11.5017 5.15027 11.4049C5.1253 11.1189 5.06701 10.861 4.99109 10.6334C4.75475 9.92518 4.35324 9.51044 4.30554 9.46155C4.30554 9.46155 4.27494 9.43195 4.21179 9.37952C4.15207 9.32993 4.06961 9.26511 3.96863 9.19397C3.76515 9.05064 3.49524 8.88718 3.19031 8.76428C3.05151 8.70835 2.90782 8.66129 2.7616 8.62854C2.61218 8.59509 2.4616 8.57679 2.31238 8.57678C1.50706 8.57678 0.918891 8.07006 0.921753 7.3844C0.924612 6.70329 1.51125 6.19594 2.31238 6.19592C2.47154 6.19591 2.6213 6.17821 2.7616 6.14709C2.90554 6.11514 3.04128 6.06904 3.16687 6.01428C3.64904 5.80395 3.97684 5.47074 4.1239 5.31995C4.12648 5.3173 4.12918 5.31473 4.13171 5.31213C4.27635 5.16393 4.73656 4.68608 4.98914 3.93518C5.06511 3.70931 5.12247 3.45905 5.14636 3.18518C5.15436 3.09338 5.15905 2.99838 5.15906 2.901C5.15906 2.44078 5.26453 2.04187 5.47156 1.7428C5.68108 1.44033 6.00279 1.23595 6.37585 1.23596Z"
188
188
-
fill={theme.colors["accent-1"]}
189
189
-
stroke={theme.colors["bg-leaflet"]}
190
190
-
strokeLinecap="round"
191
191
-
strokeLinejoin="round"
192
192
-
/>
193
193
-
</svg>
194
194
-
);
195
195
-
};
-133
app/(home-pages)/discover/getPublications.ts
···
1
1
-
"use server";
2
2
-
3
3
-
import { supabaseServerClient } from "supabase/serverClient";
4
4
-
import {
5
5
-
normalizePublicationRow,
6
6
-
hasValidPublication,
7
7
-
} from "src/utils/normalizeRecords";
8
8
-
import { deduplicateByUri } from "src/utils/deduplicateRecords";
9
9
-
10
10
-
export type Cursor = {
11
11
-
sort_date?: string;
12
12
-
count?: number;
13
13
-
uri: string;
14
14
-
};
15
15
-
16
16
-
export type Publication = Awaited<
17
17
-
ReturnType<typeof getPublications>
18
18
-
>["publications"][number];
19
19
-
20
20
-
export async function getPublications(
21
21
-
order: "recentlyUpdated" | "popular" = "recentlyUpdated",
22
22
-
cursor?: Cursor | null,
23
23
-
): Promise<{ publications: any[]; nextCursor: Cursor | null }> {
24
24
-
const limit = 25;
25
25
-
26
26
-
// Fetch all publications with their most recent document
27
27
-
let { data: publications, error } = await supabaseServerClient
28
28
-
.from("publications")
29
29
-
.select(
30
30
-
"*, documents_in_publications(*, documents(*)), publication_subscriptions(count)",
31
31
-
)
32
32
-
.or(
33
33
-
"record->preferences->showInDiscover.is.null,record->preferences->>showInDiscover.eq.true",
34
34
-
)
35
35
-
.order("documents(sort_date)", {
36
36
-
referencedTable: "documents_in_publications",
37
37
-
ascending: false,
38
38
-
})
39
39
-
.limit(1, { referencedTable: "documents_in_publications" });
40
40
-
41
41
-
if (error) {
42
42
-
console.error("Error fetching publications:", error);
43
43
-
return { publications: [], nextCursor: null };
44
44
-
}
45
45
-
46
46
-
// Deduplicate records that may exist under both pub.leaflet and site.standard namespaces
47
47
-
const dedupedPublications = deduplicateByUri(publications || []);
48
48
-
49
49
-
// Filter out publications without documents
50
50
-
const allPubs = dedupedPublications.filter(
51
51
-
(pub) => pub.documents_in_publications.length > 0,
52
52
-
);
53
53
-
54
54
-
// Sort on the server
55
55
-
allPubs.sort((a, b) => {
56
56
-
if (order === "popular") {
57
57
-
const aCount = a.publication_subscriptions[0]?.count || 0;
58
58
-
const bCount = b.publication_subscriptions[0]?.count || 0;
59
59
-
if (bCount !== aCount) {
60
60
-
return bCount - aCount;
61
61
-
}
62
62
-
// Secondary sort by uri for stability
63
63
-
return b.uri.localeCompare(a.uri);
64
64
-
} else {
65
65
-
// recentlyUpdated
66
66
-
const aDate = new Date(
67
67
-
a.documents_in_publications[0]?.documents?.sort_date || 0,
68
68
-
).getTime();
69
69
-
const bDate = new Date(
70
70
-
b.documents_in_publications[0]?.documents?.sort_date || 0,
71
71
-
).getTime();
72
72
-
if (bDate !== aDate) {
73
73
-
return bDate - aDate;
74
74
-
}
75
75
-
// Secondary sort by uri for stability
76
76
-
return b.uri.localeCompare(a.uri);
77
77
-
}
78
78
-
});
79
79
-
80
80
-
// Find cursor position and slice
81
81
-
let startIndex = 0;
82
82
-
if (cursor) {
83
83
-
startIndex = allPubs.findIndex((pub) => {
84
84
-
if (order === "popular") {
85
85
-
const pubCount = pub.publication_subscriptions[0]?.count || 0;
86
86
-
// Find first pub after cursor
87
87
-
return (
88
88
-
pubCount < (cursor.count || 0) ||
89
89
-
(pubCount === cursor.count && pub.uri < cursor.uri)
90
90
-
);
91
91
-
} else {
92
92
-
const pubDate = pub.documents_in_publications[0]?.documents?.sort_date || "";
93
93
-
// Find first pub after cursor
94
94
-
return (
95
95
-
pubDate < (cursor.sort_date || "") ||
96
96
-
(pubDate === cursor.sort_date && pub.uri < cursor.uri)
97
97
-
);
98
98
-
}
99
99
-
});
100
100
-
// If not found, we're at the end
101
101
-
if (startIndex === -1) {
102
102
-
return { publications: [], nextCursor: null };
103
103
-
}
104
104
-
}
105
105
-
106
106
-
// Get the page
107
107
-
const page = allPubs.slice(startIndex, startIndex + limit);
108
108
-
109
109
-
// Normalize publication records
110
110
-
const normalizedPage = page
111
111
-
.map(normalizePublicationRow)
112
112
-
.filter(hasValidPublication);
113
113
-
114
114
-
// Create next cursor based on last item in normalizedPage
115
115
-
const lastItem = normalizedPage[normalizedPage.length - 1];
116
116
-
const nextCursor =
117
117
-
normalizedPage.length > 0 && startIndex + limit < allPubs.length
118
118
-
? order === "recentlyUpdated"
119
119
-
? {
120
120
-
sort_date: lastItem.documents_in_publications[0]?.documents?.sort_date,
121
121
-
uri: lastItem.uri,
122
122
-
}
123
123
-
: {
124
124
-
count: lastItem.publication_subscriptions[0]?.count || 0,
125
125
-
uri: lastItem.uri,
126
126
-
}
127
127
-
: null;
128
128
-
129
129
-
return {
130
130
-
publications: normalizedPage,
131
131
-
nextCursor,
132
132
-
};
133
133
-
}
-53
app/(home-pages)/discover/page.tsx
···
1
1
-
import Link from "next/link";
2
2
-
import { SortedPublicationList } from "./SortedPublicationList";
3
3
-
import { Metadata } from "next";
4
4
-
import { DashboardLayout } from "components/PageLayouts/DashboardLayout";
5
5
-
import { getPublications } from "./getPublications";
6
6
-
7
7
-
export const metadata: Metadata = {
8
8
-
title: "Leaflet Discover",
9
9
-
description: "Explore publications on Leaflet ✨ Or make your own!",
10
10
-
};
11
11
-
12
12
-
export default async function Discover(props: {
13
13
-
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
14
14
-
}) {
15
15
-
let order = ((await props.searchParams).order as string) || "recentlyUpdated";
16
16
-
17
17
-
return (
18
18
-
<DashboardLayout
19
19
-
id="discover"
20
20
-
currentPage="discover"
21
21
-
defaultTab="default"
22
22
-
actions={null}
23
23
-
tabs={{
24
24
-
default: {
25
25
-
controls: null,
26
26
-
content: <DiscoverContent order={order} />,
27
27
-
},
28
28
-
}}
29
29
-
/>
30
30
-
);
31
31
-
}
32
32
-
33
33
-
const DiscoverContent = async (props: { order: string }) => {
34
34
-
const orderValue = props.order === "popular" ? "popular" : "recentlyUpdated";
35
35
-
let { publications, nextCursor } = await getPublications(orderValue);
36
36
-
37
37
-
return (
38
38
-
<div className="max-w-prose mx-auto w-full">
39
39
-
<div className="discoverHeader flex flex-col items-center text-center pt-2 px-4">
40
40
-
<h1>Discover</h1>
41
41
-
<p className="text-lg text-secondary italic mb-2">
42
42
-
Explore publications on Leaflet ✨ Or{" "}
43
43
-
<Link href="/lish/createPub">make your own</Link>!
44
44
-
</p>
45
45
-
</div>
46
46
-
<SortedPublicationList
47
47
-
publications={publications}
48
48
-
order={props.order}
49
49
-
nextCursor={nextCursor}
50
50
-
/>
51
51
-
</div>
52
52
-
);
53
53
-
};
+2
-2
app/(home-pages)/p/[didOrHandle]/ProfileLayout.tsx
···
11
11
${
12
12
cardBorderHidden
13
13
? ""
14
14
-
: "overflow-y-scroll h-full border border-border-light rounded-lg bg-bg-page"
14
14
+
: "overflow-y-scroll h-full border border-border-light rounded-lg bg-bg-page px-3 sm:px-4"
15
15
}
16
16
max-w-prose mx-auto w-full
17
17
-
flex flex-col
17
17
+
flex flex-col pb-3
18
18
text-center
19
19
`}
20
20
>
+1
-1
app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx
···
85
85
}
86
86
87
87
return (
88
88
-
<div className="flex flex-col gap-2 text-left relative">
88
88
+
<div className="flex flex-col gap-2 py-4 text-left relative">
89
89
{allComments.map((comment) => (
90
90
<CommentItem key={comment.uri} comment={comment} />
91
91
))}
+1
-1
app/(home-pages)/p/[didOrHandle]/subscriptions/SubscriptionsContent.tsx
···
2
2
3
3
import { useEffect, useRef } from "react";
4
4
import useSWRInfinite from "swr/infinite";
5
5
-
import { PubListing } from "app/(home-pages)/discover/PubListing";
5
5
+
import { PubListing } from "app/(home-pages)/p/[didOrHandle]/PubListing";
6
6
import {
7
7
getSubscriptions,
8
8
type PublicationSubscription,
-105
app/(home-pages)/reader/SubscriptionsContent.tsx
···
1
1
-
"use client";
2
2
-
import { PubListing } from "app/(home-pages)/discover/PubListing";
3
3
-
import { ButtonPrimary } from "components/Buttons";
4
4
-
import { DiscoverSmall } from "components/Icons/DiscoverSmall";
5
5
-
import { Json } from "supabase/database.types";
6
6
-
import { PublicationSubscription, getSubscriptions } from "./getSubscriptions";
7
7
-
import useSWRInfinite from "swr/infinite";
8
8
-
import { useEffect, useRef } from "react";
9
9
-
import { Cursor } from "./getReaderFeed";
10
10
-
import Link from "next/link";
11
11
-
12
12
-
export const SubscriptionsContent = (props: {
13
13
-
publications: PublicationSubscription[];
14
14
-
nextCursor: Cursor | null;
15
15
-
}) => {
16
16
-
const getKey = (
17
17
-
pageIndex: number,
18
18
-
previousPageData: {
19
19
-
subscriptions: PublicationSubscription[];
20
20
-
nextCursor: Cursor | null;
21
21
-
} | null,
22
22
-
) => {
23
23
-
// Reached the end
24
24
-
if (previousPageData && !previousPageData.nextCursor) return null;
25
25
-
26
26
-
// First page, we don't have previousPageData
27
27
-
if (pageIndex === 0) return ["subscriptions", null] as const;
28
28
-
29
29
-
// Add the cursor to the key
30
30
-
return ["subscriptions", previousPageData?.nextCursor] as const;
31
31
-
};
32
32
-
33
33
-
const { data, error, size, setSize, isValidating } = useSWRInfinite(
34
34
-
getKey,
35
35
-
([_, cursor]) => getSubscriptions(null, cursor),
36
36
-
{
37
37
-
fallbackData: [
38
38
-
{ subscriptions: props.publications, nextCursor: props.nextCursor },
39
39
-
],
40
40
-
revalidateFirstPage: false,
41
41
-
},
42
42
-
);
43
43
-
44
44
-
const loadMoreRef = useRef<HTMLDivElement>(null);
45
45
-
46
46
-
// Set up intersection observer to load more when trigger element is visible
47
47
-
useEffect(() => {
48
48
-
const observer = new IntersectionObserver(
49
49
-
(entries) => {
50
50
-
if (entries[0].isIntersecting && !isValidating) {
51
51
-
const hasMore = data && data[data.length - 1]?.nextCursor;
52
52
-
if (hasMore) {
53
53
-
setSize(size + 1);
54
54
-
}
55
55
-
}
56
56
-
},
57
57
-
{ threshold: 0.1 },
58
58
-
);
59
59
-
60
60
-
if (loadMoreRef.current) {
61
61
-
observer.observe(loadMoreRef.current);
62
62
-
}
63
63
-
64
64
-
return () => observer.disconnect();
65
65
-
}, [data, size, setSize, isValidating]);
66
66
-
67
67
-
const allPublications = data
68
68
-
? data.flatMap((page) => page.subscriptions)
69
69
-
: [];
70
70
-
71
71
-
if (allPublications.length === 0 && !isValidating)
72
72
-
return <SubscriptionsEmpty />;
73
73
-
74
74
-
return (
75
75
-
<div className="relative">
76
76
-
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3">
77
77
-
{allPublications?.map((p, index) => <PubListing key={p.uri} {...p} />)}
78
78
-
</div>
79
79
-
{/* Trigger element for loading more subscriptions */}
80
80
-
<div
81
81
-
ref={loadMoreRef}
82
82
-
className="absolute bottom-96 left-0 w-full h-px pointer-events-none"
83
83
-
aria-hidden="true"
84
84
-
/>
85
85
-
{isValidating && (
86
86
-
<div className="text-center text-tertiary py-4">
87
87
-
Loading more subscriptions...
88
88
-
</div>
89
89
-
)}
90
90
-
</div>
91
91
-
);
92
92
-
};
93
93
-
94
94
-
export const SubscriptionsEmpty = () => {
95
95
-
return (
96
96
-
<div className="flex flex-col gap-2 container bg-[rgba(var(--bg-page),.7)] sm:p-4 p-3 justify-between text-center text-tertiary">
97
97
-
You haven't subscribed to any publications yet!
98
98
-
<Link href={"/discover"}>
99
99
-
<ButtonPrimary className="mx-auto place-self-center">
100
100
-
<DiscoverSmall /> Discover Publications
101
101
-
</ButtonPrimary>
102
102
-
</Link>
103
103
-
</div>
104
104
-
);
105
105
-
};
+6
-4
app/(home-pages)/reader/getSubscriptions.ts
···
29
29
30
30
let query = supabaseServerClient
31
31
.from("publication_subscriptions")
32
32
-
.select(`*, publications(*, documents_in_publications(*, documents(*)))`)
32
32
+
.select(
33
33
+
`*, publications(*, publication_subscriptions(*), documents_in_publications(*, documents(*)))`,
34
34
+
)
33
35
.order(`created_at`, { ascending: false })
34
36
.order(`uri`, { ascending: false })
35
37
.order("documents(sort_date)", {
···
51
53
await Promise.all(
52
54
pubs?.map(async (pub) => {
53
55
const normalizedRecord = normalizePublicationRecord(
54
54
-
pub.publications?.record
56
56
+
pub.publications?.record,
55
57
);
56
58
if (!normalizedRecord) return null;
57
59
let id = await idResolver.did.resolve(pub.publications?.identity_did!);
···
62
64
? { handle: `@${id.alsoKnownAs[0].slice(5)}` }
63
65
: undefined,
64
66
} as PublicationSubscription;
65
65
-
}) || []
67
67
+
}) || [],
66
68
)
67
69
).filter((sub): sub is PublicationSubscription => sub !== null);
68
68
-
69
70
const nextCursor =
70
71
pubs && pubs.length > 0
71
72
? {
···
83
84
export type PublicationSubscription = {
84
85
authorProfile?: { handle: string };
85
86
record: NormalizedPublication;
87
87
+
publication_subscriptions: { identity: string }[];
86
88
uri: string;
87
89
documents_in_publications: {
88
90
documents: { data?: Json; sort_date: string } | null;
+47
-9
app/lish/Subscribe.tsx
···
24
24
import LoginForm from "app/login/LoginForm";
25
25
import { RSSSmall } from "components/Icons/RSSSmall";
26
26
import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError";
27
27
+
import { RSSTiny } from "components/Icons/RSSTiny";
27
28
28
29
export const SubscribeWithBluesky = (props: {
30
30
+
compact?: boolean;
29
31
pubName: string;
30
32
pub_uri: string;
31
33
base_url: string;
···
36
38
let [successModalOpen, setSuccessModalOpen] = useState(
37
39
!!searchParams.has("showSubscribeSuccess"),
38
40
);
41
41
+
let [localSubscribeState, setLocalSubscribeState] = useState<
42
42
+
"subscribed" | "unsubscribed"
43
43
+
>("subscribed");
39
44
let subscribed =
40
45
identity?.atp_did &&
46
46
+
localSubscribeState !== "unsubscribed" &&
41
47
props.subscribers.find((s) => s.identity === identity.atp_did);
42
48
43
49
if (successModalOpen)
···
48
54
/>
49
55
);
50
56
if (subscribed) {
51
51
-
return <ManageSubscription {...props} />;
57
57
+
return (
58
58
+
<ManageSubscription
59
59
+
{...props}
60
60
+
onUnsubscribe={() => setLocalSubscribeState("unsubscribed")}
61
61
+
/>
62
62
+
);
52
63
}
53
64
return (
54
65
<div className="flex flex-col gap-2 text-center justify-center">
55
66
<div className="flex flex-row gap-2 place-self-center">
56
67
<BlueskySubscribeButton
68
68
+
setLocalSubscribeState={() => setLocalSubscribeState("subscribed")}
69
69
+
compact={props.compact}
57
70
pub_uri={props.pub_uri}
58
71
setSuccessModalOpen={setSuccessModalOpen}
59
72
/>
···
63
76
target="_blank"
64
77
aria-label="Subscribe to RSS"
65
78
>
66
66
-
<RSSSmall className="self-center" aria-hidden />
79
79
+
{props.compact ? (
80
80
+
<RSSTiny className="self-center" aria-hidden />
81
81
+
) : (
82
82
+
<RSSSmall className="self-center" aria-hidden />
83
83
+
)}
67
84
</a>
68
85
</div>
69
86
</div>
···
74
91
pub_uri: string;
75
92
subscribers: { identity: string }[];
76
93
base_url: string;
94
94
+
compact?: boolean;
95
95
+
onUnsubscribe?: () => void;
77
96
}) => {
78
97
let toaster = useToaster();
79
98
let [hasFeed] = useState(false);
···
83
102
content: "You unsubscribed.",
84
103
type: "success",
85
104
});
105
105
+
props.onUnsubscribe?.();
86
106
}, null);
87
107
return (
88
108
<Popover
89
109
trigger={
90
90
-
<div className="text-accent-contrast text-sm w-fit">
110
110
+
<div
111
111
+
className={`text-accent-contrast w-fit ${props.compact ? "text-xs" : "text-sm"}`}
112
112
+
>
91
113
Manage Subscription
92
114
</div>
93
115
}
94
116
>
95
95
-
<div className="max-w-sm flex flex-col gap-1">
117
117
+
<div
118
118
+
className={`max-w-sm flex flex-col gap-1 ${props.compact && "text-sm"}`}
119
119
+
>
96
120
<h4>Update Options</h4>
97
121
98
122
{!hasFeed && (
···
102
126
className=" place-self-center"
103
127
>
104
128
<ButtonPrimary fullWidth compact className="!px-4">
105
105
-
View Bluesky Custom Feed
129
129
+
Bluesky Custom Feed
106
130
</ButtonPrimary>
107
131
</a>
108
132
)}
···
121
145
<hr className="border-border-light my-1" />
122
146
123
147
<form action={unsubscribe}>
124
124
-
<button className="font-bold text-accent-contrast w-max place-self-center">
125
125
-
{unsubscribePending ? <DotLoader /> : "Unsubscribe"}
148
148
+
<button className="font-bold w-full text-accent-contrast text-center mx-auto">
149
149
+
{unsubscribePending ? (
150
150
+
<DotLoader className="w-fit mx-auto" />
151
151
+
) : (
152
152
+
"Unsubscribe"
153
153
+
)}
126
154
</button>
127
155
</form>
128
156
</div>
···
133
161
let BlueskySubscribeButton = (props: {
134
162
pub_uri: string;
135
163
setSuccessModalOpen: (open: boolean) => void;
164
164
+
compact?: boolean;
165
165
+
setLocalSubscribeState: () => void;
136
166
}) => {
137
167
let { identity } = useIdentityData();
138
168
let toaster = useToaster();
···
155
185
props.setSuccessModalOpen(true);
156
186
}
157
187
toaster({ content: <div>You're Subscribed!</div>, type: "success" });
188
188
+
props.setLocalSubscribeState();
158
189
}, null);
159
190
160
191
let [isClient, setIsClient] = useState(false);
···
166
197
return (
167
198
<Popover
168
199
asChild
200
200
+
className="max-w-xs"
169
201
trigger={
170
170
-
<ButtonPrimary className="place-self-center">
202
202
+
<ButtonPrimary
203
203
+
compact={props.compact}
204
204
+
className={`place-self-center ${props.compact && "text-sm"}`}
205
205
+
>
171
206
<BlueskyTiny /> Subscribe with Bluesky
172
207
</ButtonPrimary>
173
208
}
···
190
225
action={subscribe}
191
226
className="place-self-center flex flex-row gap-1"
192
227
>
193
193
-
<ButtonPrimary>
228
228
+
<ButtonPrimary
229
229
+
compact={props.compact}
230
230
+
className={props.compact ? "text-sm" : ""}
231
231
+
>
194
232
{subscribePending ? (
195
233
<DotLoader />
196
234
) : (
+1
-1
components/ActionBar/ProfileButton.tsx
···
40
40
>
41
41
{record && (
42
42
<>
43
43
-
<SpeedyLink href={`/p/${record.handle}`}>
43
43
+
<SpeedyLink className="no-underline!" href={`/p/${record.handle}`}>
44
44
<MenuItem onSelect={() => {}}>View Profile</MenuItem>
45
45
</SpeedyLink>
46
46
+18
components/Icons/RSSTiny.tsx
···
1
1
+
import { Props } from "./Props";
2
2
+
export const RSSTiny = (props: Props) => {
3
3
+
return (
4
4
+
<svg
5
5
+
width="16"
6
6
+
height="16"
7
7
+
viewBox="0 0 16 16"
8
8
+
fill="none"
9
9
+
xmlns="http://www.w3.org/2000/svg"
10
10
+
{...props}
11
11
+
>
12
12
+
<path
13
13
+
d="M2.82098 5.7636C6.84291 5.76364 10.2363 8.92669 10.2364 13.179C10.2364 13.8688 9.67713 14.428 8.98738 14.428C8.29764 14.428 7.73841 13.8688 7.73837 13.179C7.7383 10.3543 5.5118 8.26167 2.82098 8.26163C2.13119 8.26163 1.572 7.7024 1.57196 7.01262C1.57196 6.32281 2.13116 5.7636 2.82098 5.7636ZM2.82098 1.57196C9.12441 1.572 14.428 6.52137 14.428 13.179C14.428 13.8688 13.8688 14.428 13.179 14.428C12.4892 14.428 11.93 13.8688 11.93 13.179C11.93 7.94901 7.7933 4.07003 2.82098 4.06999C2.13116 4.06999 1.57196 3.51079 1.57196 2.82098C1.57196 2.13116 2.13116 1.57196 2.82098 1.57196ZM3.93094 10.6066C4.82318 10.6067 5.54639 11.3299 5.54649 12.2221C5.54649 13.1145 4.82325 13.8382 3.93094 13.8383C3.03853 13.8383 2.31478 13.1145 2.31478 12.2221C2.31489 11.3298 3.03859 10.6066 3.93094 10.6066Z"
14
14
+
fill="currentColor"
15
15
+
/>
16
16
+
</svg>
17
17
+
);
18
18
+
};
+1
-1
components/Popover/index.tsx
···
42
42
<NestedCardThemeProvider>
43
43
<RadixPopover.Content
44
44
className={`
45
45
-
z-20 bg-bg-page
45
45
+
z-20 relative bg-bg-page
46
46
px-3 py-2 text-primary
47
47
max-w-(--radix-popover-content-available-width)
48
48
max-h-(--radix-popover-content-available-height)