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+
125type Contact = {
2- url : string ;
3- platform ?: string ;
4- }
26+ url : string ;
27+ platform ?: string ;
28+ } ;
529type Card = {
6- name : string ;
7- contacts : Contact [ ] ;
30+ name : string ;
31+ contacts : Contact [ ] ;
832} ;
933
1034type 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
1943class 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