Skip to content

Commit 754e49a

Browse files
get PDS link component to work
1 parent c15ba37 commit 754e49a

File tree

4 files changed

+303
-53
lines changed

4 files changed

+303
-53
lines changed

webcomponent-pds-links/__example__/package-lock.json

Lines changed: 105 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webcomponent-pds-links/__example__/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"astro": "astro"
1111
},
1212
"dependencies": {
13+
"@atproto/identity": "^0.4.9",
14+
"@fujocoded/zod-transform-socials": "^0.0.12",
1315
"astro": "^5.12.9"
1416
},
1517
"devDependencies": {
Lines changed: 145 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,161 @@
1+
import {
2+
SocialLinks,
3+
type SocialLinksData,
4+
} from "@fujocoded/zod-transform-socials";
5+
import { IdResolver } from "@atproto/identity";
6+
7+
const IDENTITY_RESOLVER = new IdResolver({});
8+
9+
export const getDid = async ({ didOrHandle }: { didOrHandle: string }) => {
10+
if (didOrHandle.startsWith("did:")) {
11+
return didOrHandle;
12+
}
13+
return await IDENTITY_RESOLVER.handle.resolve(didOrHandle);
14+
};
15+
16+
export const getPdsUrl = async ({ didOrHandle }: { didOrHandle: string }) => {
17+
const did = await getDid({ didOrHandle });
18+
if (!did) {
19+
throw new Error(`Did not resolve to a valid DID: ${didOrHandle}`);
20+
}
21+
const atprotoData = await IDENTITY_RESOLVER.did.resolveAtprotoData(did);
22+
return atprotoData.pds;
23+
};
24+
125
type Contact = {
2-
url: string;
3-
platform?: string;
4-
}
26+
url: string;
27+
platform?: string;
28+
};
529
type Card = {
6-
name: string;
7-
contacts: Contact[];
30+
name: string;
31+
contacts: Contact[];
832
};
933

1034
type Collections = {
11-
cursor: string | undefined;
12-
records: {
13-
cid: string;
14-
uri: string;
15-
value: Card
16-
}[];
17-
}
35+
cursor: string | undefined;
36+
records: {
37+
cid: string;
38+
uri: string;
39+
value: Card;
40+
}[];
41+
};
1842

1943
class PdsLinks extends HTMLElement {
20-
static observedAttributes = ["did", "pds-url"];
44+
static observedAttributes = ["did", "handle", "at-identifier"];
45+
46+
constructor() {
47+
super();
48+
}
2149

22-
constructor() {
23-
super();
50+
async getPdsData() {
51+
const userId =
52+
this.getAttribute("did") ??
53+
this.getAttribute("handle") ??
54+
this.getAttribute("at-identifier");
55+
if (!userId) {
56+
throw new Error("Couldn't find a valid UserId");
2457
}
58+
const did = await getDid({ didOrHandle: userId });
59+
return {
60+
did: did!,
61+
url: await getPdsUrl({ didOrHandle: userId }),
62+
};
63+
}
2564

26-
updateContacts(card: Card) {
27-
const template = this.querySelector("template");
28-
console.log(template);
29-
if (!template) {
30-
return;
31-
}
32-
const contactElements = card.contacts.map(contact => {
33-
const contactElement = template?.content.firstElementChild?.cloneNode(true) as HTMLLIElement;
34-
const linkElement = contactElement.querySelector("a")!;
35-
linkElement.href = contact.url;
36-
linkElement.dataset.platform = contact.platform;
37-
linkElement.innerText = contact.url;
38-
return contactElement;
39-
});
40-
console.log(contactElements);
41-
this.replaceChildren(template, ...contactElements);
65+
/**
66+
* Returns the element with the given tag from the given parent, or
67+
* creates and appends it if there's none.
68+
*
69+
* Additionally, if no parent is given, return that Element in the template,
70+
* or make the given one as a new root and append it.
71+
*/
72+
getOrCreateElement<T extends HTMLElement>(
73+
elementTag: keyof HTMLElementTagNameMap,
74+
parent?: HTMLElement
75+
): T {
76+
if (!parent) {
77+
const template = this.querySelector("template")?.content.cloneNode(
78+
true
79+
) as DocumentFragment | undefined;
80+
if (template?.querySelector(elementTag)) {
81+
const newTag = this.appendChild(template.querySelector(elementTag)!)
82+
return newTag as T;
83+
}
84+
} else {
85+
const innerElement = parent.querySelector(elementTag);
86+
if (innerElement) {
87+
return innerElement as T;
88+
}
4289
}
90+
const newElement = document.createElement(elementTag) as T;
91+
(parent ?? this).appendChild(newElement);
92+
return newElement;
93+
}
4394

44-
async connectedCallback() {
45-
const did = this.getAttribute("did")!;
46-
const pdsUrl = this.getAttribute("pds-url")!;
47-
const getRecordsEndpoint = new URL("/xrpc/com.atproto.repo.listRecords", pdsUrl);
48-
getRecordsEndpoint.searchParams.set("repo", did);
49-
getRecordsEndpoint.searchParams.set("collection", "com.fujocoded.rolodex.card");
50-
51-
const collectionsResponse = await fetch(getRecordsEndpoint);
52-
if (collectionsResponse.ok) {
53-
const collections = await collectionsResponse.json();
54-
console.log(collections);
55-
this.updateContacts((collections as unknown as Collections).records[0].value)
56-
}
95+
getInnerLinkText(contact: SocialLinksData[number]) {
96+
const name =
97+
contact.username ??
98+
contact.url.replace("http://", "").replace("https://", "");
99+
return name.endsWith("/") ? name.substring(0, name.length - 1) : name;
100+
}
101+
102+
getIcon(contact: SocialLinksData[number]) {
103+
if (contact.icon?.startsWith("simple-icons:")) {
104+
const iconName = contact.icon.replace("simple-icons:", "");
105+
return `https://cdn.simpleicons.org/${iconName}`;
57106
}
58107

59-
// attributeChangedCallback(name: string, oldValue: string, newValue: string) {
60-
// console.log(name, oldValue, newValue)
61-
// }
62-
}
108+
if (contact.icon == "favicon") {
109+
return new URL("/favicon/favicon-32x32.png", contact.url).href;
110+
}
111+
}
112+
113+
updateContacts(card: Card) {
114+
const contactElements = SocialLinks.parse(card.contacts).map((contact) => {
115+
const contactElement = this.getOrCreateElement<HTMLLIElement>("li");
116+
const linkElement = this.getOrCreateElement<HTMLAnchorElement>(
117+
"a",
118+
contactElement
119+
);
120+
121+
linkElement.href = contact.url;
122+
linkElement.dataset.platform = contact.platform;
123+
linkElement.innerText = this.getInnerLinkText(contact);
124+
125+
const iconUrl = this.getIcon(contact);
126+
if (iconUrl) {
127+
contactElement.dataset.icon = iconUrl;
128+
contactElement.style.setProperty("--icon-url", `url(${iconUrl})`)
129+
}
63130

131+
return contactElement;
132+
});
133+
const root = this.getOrCreateElement("ul");
134+
root.replaceChildren(this.querySelector("template") ?? "", ...contactElements);
135+
}
136+
137+
async connectedCallback() {
138+
// As soon as we latch in, we go get the data from the PDS
139+
const pdsData = await this.getPdsData();
140+
141+
const getRecordsEndpoint = new URL(
142+
"/xrpc/com.atproto.repo.listRecords",
143+
pdsData.url
144+
);
145+
getRecordsEndpoint.searchParams.set("repo", pdsData.did);
146+
getRecordsEndpoint.searchParams.set(
147+
"collection",
148+
"com.fujocoded.rolodex.card"
149+
);
150+
151+
const collectionsResponse = await fetch(getRecordsEndpoint);
152+
if (collectionsResponse.ok) {
153+
const collections = await collectionsResponse.json();
154+
this.updateContacts(
155+
(collections as unknown as Collections).records[0].value
156+
);
157+
}
158+
}
159+
}
64160

65-
customElements.define("atfujo-links", PdsLinks)
161+
customElements.define("atfujo-links", PdsLinks);

0 commit comments

Comments
 (0)