+71
-76
src/components/backlinks.tsx
+71
-76
src/components/backlinks.tsx
···
24
24
const Backlinks = (props: { target: string }) => {
25
25
const fetchBacklinks = async () => {
26
26
const res = await getAllBacklinks(props.target);
27
-
setBacklinks(linksBySource(res.links));
28
-
return res;
27
+
return linksBySource(res.links);
29
28
};
30
29
31
30
const [response] = createResource(fetchBacklinks);
32
-
const [backlinks, setBacklinks] = createSignal<any>();
33
31
34
32
const [show, setShow] = createSignal<{
35
33
collection: string;
···
38
36
} | null>();
39
37
40
38
return (
41
-
<Show when={response()}>
42
-
<div class="flex w-full flex-col gap-1 text-sm wrap-anywhere">
43
-
<For each={backlinks()}>
44
-
{({ collection, path, counts }) => (
39
+
<div class="flex w-full flex-col gap-1 text-sm wrap-anywhere">
40
+
<Show when={response()?.length === 0}>
41
+
<p>No backlinks found.</p>
42
+
</Show>
43
+
<For each={response()}>
44
+
{({ collection, path, counts }) => (
45
+
<div>
45
46
<div>
46
-
<div>
47
-
<div title="Collection containing linking records" class="flex items-center gap-1">
48
-
<span class="iconify lucide--book-text shrink-0"></span>
49
-
{collection}
50
-
</div>
51
-
<div title="Record path where the link is found" class="flex items-center gap-1">
52
-
<span class="iconify lucide--route shrink-0"></span>
53
-
{path.slice(1)}
54
-
</div>
47
+
<div title="Collection containing linking records" class="flex items-center gap-1">
48
+
<span class="iconify lucide--book-text shrink-0"></span>
49
+
{collection}
50
+
</div>
51
+
<div title="Record path where the link is found" class="flex items-center gap-1">
52
+
<span class="iconify lucide--route shrink-0"></span>
53
+
{path.slice(1)}
55
54
</div>
56
-
<div class="ml-4.5">
57
-
<p>
58
-
<button
59
-
class="text-blue-400 hover:underline active:underline"
60
-
title="Show linking records"
61
-
onclick={() =>
62
-
(
63
-
show()?.collection === collection &&
64
-
show()?.path === path &&
65
-
!show()?.showDids
66
-
) ?
67
-
setShow(null)
68
-
: setShow({ collection, path, showDids: false })
69
-
}
70
-
>
71
-
{counts.records} record{counts.records < 2 ? "" : "s"}
72
-
</button>
73
-
{" from "}
74
-
<button
75
-
class="text-blue-400 hover:underline active:underline"
76
-
title="Show linking DIDs"
77
-
onclick={() =>
78
-
(
79
-
show()?.collection === collection &&
80
-
show()?.path === path &&
81
-
show()?.showDids
82
-
) ?
83
-
setShow(null)
84
-
: setShow({ collection, path, showDids: true })
85
-
}
86
-
>
87
-
{counts.distinct_dids} DID
88
-
{counts.distinct_dids < 2 ? "" : "s"}
89
-
</button>
90
-
</p>
91
-
<Show when={show()?.collection === collection && show()?.path === path}>
92
-
<Show when={show()?.showDids}>
93
-
{/* putting this in the `dids` prop directly failed to re-render. idk how to solidjs. */}
94
-
<p class="w-full font-semibold">Distinct identities</p>
95
-
<BacklinkItems
96
-
target={props.target}
97
-
collection={collection}
98
-
path={path}
99
-
dids={true}
100
-
/>
101
-
</Show>
102
-
<Show when={!show()?.showDids}>
103
-
<p class="w-full font-semibold">Records</p>
104
-
<BacklinkItems
105
-
target={props.target}
106
-
collection={collection}
107
-
path={path}
108
-
dids={false}
109
-
/>
110
-
</Show>
55
+
</div>
56
+
<div class="ml-4.5">
57
+
<p>
58
+
<button
59
+
class="text-blue-400 hover:underline active:underline"
60
+
title="Show linking records"
61
+
onclick={() =>
62
+
(
63
+
show()?.collection === collection &&
64
+
show()?.path === path &&
65
+
!show()?.showDids
66
+
) ?
67
+
setShow(null)
68
+
: setShow({ collection, path, showDids: false })
69
+
}
70
+
>
71
+
{counts.records} record{counts.records < 2 ? "" : "s"}
72
+
</button>
73
+
{" from "}
74
+
<button
75
+
class="text-blue-400 hover:underline active:underline"
76
+
title="Show linking DIDs"
77
+
onclick={() =>
78
+
show()?.collection === collection && show()?.path === path && show()?.showDids ?
79
+
setShow(null)
80
+
: setShow({ collection, path, showDids: true })
81
+
}
82
+
>
83
+
{counts.distinct_dids} DID
84
+
{counts.distinct_dids < 2 ? "" : "s"}
85
+
</button>
86
+
</p>
87
+
<Show when={show()?.collection === collection && show()?.path === path}>
88
+
<Show when={show()?.showDids}>
89
+
{/* putting this in the `dids` prop directly failed to re-render. idk how to solidjs. */}
90
+
<p class="w-full font-semibold">Distinct identities</p>
91
+
<BacklinkItems
92
+
target={props.target}
93
+
collection={collection}
94
+
path={path}
95
+
dids={true}
96
+
/>
97
+
</Show>
98
+
<Show when={!show()?.showDids}>
99
+
<p class="w-full font-semibold">Records</p>
100
+
<BacklinkItems
101
+
target={props.target}
102
+
collection={collection}
103
+
path={path}
104
+
dids={false}
105
+
/>
111
106
</Show>
112
-
</div>
107
+
</Show>
113
108
</div>
114
-
)}
115
-
</For>
116
-
</div>
117
-
</Show>
109
+
</div>
110
+
)}
111
+
</For>
112
+
</div>
118
113
);
119
114
};
120
115
+6
-3
src/components/create.tsx
+6
-3
src/components/create.tsx
···
172
172
></span>
173
173
<span>{props.create ? "Creating" : "Editing"} record</span>
174
174
</div>
175
-
<button onclick={() => setOpenDialog(false)} class="flex items-center">
176
-
<span class="iconify lucide--x text-lg hover:text-neutral-500 dark:hover:text-neutral-400"></span>
175
+
<button
176
+
onclick={() => setOpenDialog(false)}
177
+
class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
178
+
>
179
+
<span class="iconify lucide--x"></span>
177
180
</button>
178
181
</div>
179
182
<form ref={formRef} class="flex flex-col gap-y-2">
···
186
189
<TextInput
187
190
id="collection"
188
191
name="collection"
189
-
placeholder="Optional (default: record type)"
192
+
placeholder="Optional (default: $type)"
190
193
class="w-[15rem]"
191
194
/>
192
195
</div>
+66
-18
src/components/search.tsx
+66
-18
src/components/search.tsx
···
2
2
import { A, useLocation, useNavigate } from "@solidjs/router";
3
3
import { createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js";
4
4
import { isTouchDevice } from "../layout";
5
+
import { appHandleLink, appList, appName, AppUrl } from "../utils/app-urls";
5
6
import { createDebouncedValue } from "../utils/hooks/debounced";
7
+
import { Modal } from "./modal";
6
8
7
9
export const [showSearch, setShowSearch] = createSignal(false);
8
10
···
66
68
input = input.trim().replace(/^@/, "");
67
69
if (!input.length) return;
68
70
setShowSearch(false);
69
-
if (input === "me" && localStorage.getItem("lastSignedIn") !== null) {
70
-
navigate(`/at://${localStorage.getItem("lastSignedIn")}`);
71
-
} else if (
72
-
!input.startsWith("https://bsky.app/") &&
73
-
(input.startsWith("https://") || input.startsWith("http://"))
74
-
) {
75
-
navigate(`/${input.replace("https://", "").replace("http://", "").replace("/", "")}`);
76
-
} else if (search()?.length) {
71
+
if (search()?.length) {
77
72
navigate(`/at://${search()![0].did}`);
73
+
} else if (input.startsWith("https://") || input.startsWith("http://")) {
74
+
const hostLength = input.indexOf("/", 8);
75
+
const host = input.slice(0, hostLength).replace("https://", "").replace("http://", "");
76
+
77
+
if (!(host in appList)) {
78
+
navigate(`/${input.replace("https://", "").replace("http://", "").replace("/", "")}`);
79
+
} else {
80
+
const app = appList[host as AppUrl];
81
+
const path = input.slice(hostLength + 1).split("/");
82
+
83
+
const uri = appHandleLink[app](path);
84
+
navigate(`/${uri}`);
85
+
}
78
86
} else {
79
-
const uri = input
80
-
.replace("at://", "")
81
-
.replace("https://bsky.app/profile/", "")
82
-
.replace("/post/", "/app.bsky.feed.post/");
83
-
const uriParts = uri.split("/");
84
-
navigate(
85
-
`/at://${uriParts[0]}${uriParts.length > 1 ? `/${uriParts.slice(1).join("/")}` : ""}`,
86
-
);
87
+
navigate(`/at://${input.replace("at://", "")}`);
87
88
}
88
-
setShowSearch(false);
89
89
};
90
90
91
91
return (
···
114
114
value={input() ?? ""}
115
115
onInput={(e) => setInput(e.currentTarget.value)}
116
116
/>
117
-
<Show when={input()}>
117
+
<Show when={input()} fallback={ListUrlsTooltip()}>
118
118
<button
119
119
type="button"
120
120
class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-600 dark:active:bg-neutral-500"
···
144
144
</div>
145
145
</Show>
146
146
</form>
147
+
);
148
+
};
149
+
150
+
const ListUrlsTooltip = () => {
151
+
const [openList, setOpenList] = createSignal(false);
152
+
153
+
let urls: Record<string, AppUrl[]> = {};
154
+
for (const [appUrl, appView] of Object.entries(appList)) {
155
+
if (!urls[appView]) urls[appView] = [appUrl as AppUrl];
156
+
else urls[appView].push(appUrl as AppUrl);
157
+
}
158
+
159
+
return (
160
+
<>
161
+
<Modal open={openList()} onClose={() => setOpenList(false)}>
162
+
<div class="dark:bg-dark-300 dark:shadow-dark-800 absolute top-16 left-[50%] w-[26rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
163
+
<div class="mb-2 flex items-center gap-1 font-semibold">
164
+
<span class="iconify lucide--link"></span>
165
+
<span>Supported URLs</span>
166
+
</div>
167
+
<div class="mb-2 text-sm text-neutral-500 dark:text-neutral-400">
168
+
Links that will be parsed automatically, as long as all the data necessary is on the
169
+
URL.
170
+
</div>
171
+
<div class="flex flex-col gap-2 text-sm">
172
+
<For each={Object.entries(appName)}>
173
+
{([appView, name]) => {
174
+
return (
175
+
<div>
176
+
<p class="font-semibold">{name}</p>
177
+
<div class="grid grid-cols-2 gap-x-4 text-neutral-600 dark:text-neutral-400">
178
+
<For each={urls[appView]}>{(url) => <span>{url}</span>}</For>
179
+
</div>
180
+
</div>
181
+
);
182
+
}}
183
+
</For>
184
+
</div>
185
+
</div>
186
+
</Modal>
187
+
<button
188
+
type="button"
189
+
class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-600 dark:active:bg-neutral-500"
190
+
onClick={() => setOpenList(true)}
191
+
>
192
+
<span class="iconify lucide--help-circle"></span>
193
+
</button>
194
+
</>
147
195
);
148
196
};
149
197
+119
src/utils/app-urls.ts
+119
src/utils/app-urls.ts
···
1
+
export type AppUrl = `${string}.${string}` | `localhost:${number}`;
2
+
3
+
export enum App {
4
+
Bluesky,
5
+
Tangled,
6
+
Whitewind,
7
+
Frontpage,
8
+
Pinksea,
9
+
Linkat,
10
+
}
11
+
12
+
export const appName = {
13
+
[App.Bluesky]: "Bluesky",
14
+
[App.Tangled]: "Tangled",
15
+
[App.Whitewind]: "Whitewind",
16
+
[App.Frontpage]: "Frontpage",
17
+
[App.Pinksea]: "Pinksea",
18
+
[App.Linkat]: "Linkat",
19
+
};
20
+
21
+
export const appList: Record<AppUrl, App> = {
22
+
"localhost:19006": App.Bluesky,
23
+
"blacksky.community": App.Bluesky,
24
+
"bsky.app": App.Bluesky,
25
+
"catsky.social": App.Bluesky,
26
+
"deer.aylac.top": App.Bluesky,
27
+
"deer-social-ayla.pages.dev": App.Bluesky,
28
+
"deer.social": App.Bluesky,
29
+
"main.bsky.dev": App.Bluesky,
30
+
"social.daniela.lol": App.Bluesky,
31
+
"tangled.org": App.Tangled,
32
+
"whtwnd.com": App.Whitewind,
33
+
"frontpage.fyi": App.Frontpage,
34
+
"pinksea.art": App.Pinksea,
35
+
"linkat.blue": App.Linkat,
36
+
};
37
+
38
+
export const appHandleLink: Record<App, (url: string[]) => string> = {
39
+
[App.Bluesky]: (path) => {
40
+
const baseType = path[0];
41
+
const user = path[1];
42
+
43
+
if (baseType === "profile") {
44
+
if (path[2]) {
45
+
const type = path[2];
46
+
const rkey = path[3];
47
+
48
+
if (type === "post") {
49
+
return `at://${user}/app.bsky.feed.post/${rkey}`;
50
+
} else if (type === "list") {
51
+
return `at://${user}/app.bsky.graph.list/${rkey}`;
52
+
} else if (type === "feed") {
53
+
return `at://${user}/app.bsky.feed.generator/${rkey}`;
54
+
} else if (type === "follows") {
55
+
return `at://${user}/app.bsky.graph.follow/${rkey}`;
56
+
}
57
+
} else {
58
+
return `at://${user}`;
59
+
}
60
+
} else if (baseType === "starter-pack") {
61
+
return `at://${user}/app.bsky.graph.starterpack/${path[2]}`;
62
+
}
63
+
return `at://${user}`;
64
+
},
65
+
[App.Tangled]: (path) => {
66
+
if (path[0] === "strings") {
67
+
return `at://${path[1]}/sh.tangled.string/${path[2]}`;
68
+
}
69
+
70
+
let query: string | undefined;
71
+
if (path[path.length - 1].includes("?")) {
72
+
const split = path[path.length - 1].split("?");
73
+
query = split[1];
74
+
path[path.length - 1] = split[0];
75
+
}
76
+
77
+
const user = path[0].replace("@", "");
78
+
79
+
if (path.length === 1) {
80
+
if (query === "tab=repos") {
81
+
return `at://${user}/sh.tangled.repo`;
82
+
} else if (query === "tab=starred") {
83
+
return `at://${user}/sh.tangled.feed.star`;
84
+
} else if (query === "tab=strings") {
85
+
return `at://${user}/sh.tangled.string`;
86
+
}
87
+
} else if (path.length === 2) {
88
+
// no way to convert the repo name to an rkey afaik
89
+
// same reason why there's nothing related to issues in here
90
+
return `at://${user}/sh.tangled.repo`;
91
+
}
92
+
93
+
return `at://${user}`;
94
+
},
95
+
[App.Whitewind]: (path) => {
96
+
if (path.length === 2) {
97
+
return `at://${path[0]}/com.whtwnd.blog.entry/${path[1]}`;
98
+
}
99
+
100
+
return `at://${path[0]}/com.whtwnd.blog.entry`;
101
+
},
102
+
[App.Frontpage]: (path) => {
103
+
if (path.length === 3) {
104
+
return `at://${path[1]}/fyi.unravel.frontpage.post/${path[2]}`;
105
+
} else if (path.length === 5) {
106
+
return `at://${path[3]}/fyi.unravel.frontpage.comment/${path[4]}`;
107
+
}
108
+
109
+
return `at://${path[0]}`;
110
+
},
111
+
[App.Pinksea]: (path) => {
112
+
if (path.length === 2) {
113
+
return `at://${path[0]}/com.shinolabs.pinksea.oekaki/${path[1]}`;
114
+
}
115
+
116
+
return `at://${path[0]}`;
117
+
},
118
+
[App.Linkat]: (path) => `at://${path[0]}/blue.linkat.board/self`,
119
+
};
+1
-1
src/views/collection.tsx
+1
-1
src/views/collection.tsx
···
41
41
42
42
return (
43
43
<span
44
-
class="relative flex w-full items-baseline rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
44
+
class="relative flex w-full min-w-0 items-baseline rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
45
45
ref={rkeyRef}
46
46
onmouseover={() => setHover(true)}
47
47
onmouseleave={() => setHover(false)}
+11
-19
src/views/record.tsx
+11
-19
src/views/record.tsx
···
10
10
import { JSONValue } from "../components/json.jsx";
11
11
import { agent } from "../components/login.jsx";
12
12
import { Modal } from "../components/modal.jsx";
13
-
import { pds, setCID } from "../components/navbar.jsx";
13
+
import { pds } from "../components/navbar.jsx";
14
14
import Tooltip from "../components/tooltip.jsx";
15
15
import { setNotif } from "../layout.jsx";
16
16
import { didDocCache, resolveLexiconAuthority, resolvePDS } from "../utils/api.js";
···
34
34
let rpc: Client;
35
35
36
36
const fetchRecord = async () => {
37
-
setCID(undefined);
38
37
setValidRecord(undefined);
39
38
setValidSchema(undefined);
40
39
setLexiconUri(undefined);
···
52
51
setNotice(res.data.error);
53
52
throw new Error(res.data.error);
54
53
}
55
-
setCID(res.data.cid);
56
54
setExternalLink(checkUri(res.data.uri, res.data.value));
57
55
resolveLexicon(params.collection as Nsid);
58
56
verify(res.data);
···
198
196
label="Copy record"
199
197
icon="lucide--copy"
200
198
/>
199
+
<Show when={record()?.cid}>
200
+
{(cid) => <CopyMenu copyContent={cid()} label="Copy CID" icon="lucide--copy" />}
201
+
</Show>
202
+
<Show when={lexiconUri()}>
203
+
<NavMenu
204
+
href={`/${lexiconUri()}`}
205
+
icon="lucide--scroll-text"
206
+
label="Lexicon schema"
207
+
/>
208
+
</Show>
201
209
<Show when={externalLink()}>
202
210
{(externalLink) => (
203
211
<NavMenu
···
280
288
<span
281
289
class={`iconify ${validSchema() ? "lucide--check text-green-500 dark:text-green-400" : "lucide--x text-red-500 dark:text-red-400"}`}
282
290
></span>
283
-
</div>
284
-
</Show>
285
-
<Show when={lexiconUri()}>
286
-
<div>
287
-
<div class="flex items-center gap-1">
288
-
<span class="iconify lucide--scroll-text"></span>
289
-
<p class="font-semibold">Lexicon document</p>
290
-
</div>
291
-
<div class="truncate text-xs">
292
-
<A
293
-
href={`/${lexiconUri()}`}
294
-
class="text-blue-400 hover:underline active:underline"
295
-
>
296
-
{lexiconUri()}
297
-
</A>
298
-
</div>
299
291
</div>
300
292
</Show>
301
293
</div>