tangled
alpha
login
or
join now
witchcraft.systems
/
pds-dash
15
fork
atom
this repo has no description
15
fork
atom
overview
issues
pulls
pipelines
Somewhat sensible design (WIP)
astrra.space
10 months ago
dcca38a9
164571ec
verified
This commit was signed with the committer's
known signature
.
astrra.space
SSH Key Fingerprint:
SHA256:jQDNS75/33T59Ey4yAzrUPP/5YQaXEetsW8hwUae+ag=
+162
-75
5 changed files
expand all
collapse all
unified
split
src
App.svelte
app.css
lib
AccountComponent.svelte
PostComponent.svelte
pdsfetch.ts
+35
-1
src/App.svelte
···
7
</script>
8
9
<main>
10
-
<h1>Welcome to the Feed</h1>
11
{#await accountsPromise}
12
<p>Loading...</p>
13
{:then accountsData}
14
<div id="Account">
0
0
15
{#each accountsData as accountObject}
16
<AccountComponent account={accountObject} />
17
{/each}
···
29
{/each}
30
</div>
31
{/await}
0
32
</main>
33
34
<style>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
35
</style>
···
7
</script>
8
9
<main>
10
+
<div id="Content">
11
{#await accountsPromise}
12
<p>Loading...</p>
13
{:then accountsData}
14
<div id="Account">
15
+
<h1 id="Header">ATProto PDS</h1>
16
+
<p>Home to {accountsData.length} accounts</p>
17
{#each accountsData as accountObject}
18
<AccountComponent account={accountObject} />
19
{/each}
···
31
{/each}
32
</div>
33
{/await}
34
+
</div>
35
</main>
36
37
<style>
38
+
#Content {
39
+
display: flex;
40
+
/* split the screen in half, left for accounts, right for posts */
41
+
width: 100%;
42
+
height: 100%;
43
+
flex-direction: row;
44
+
justify-content: space-between;
45
+
align-items: center;
46
+
background-color: #12082b;
47
+
color: #ffffff;
48
+
}
49
+
#Feed {
50
+
width: 65%;
51
+
height: 80vh;
52
+
overflow-y: scroll;
53
+
padding: 20px;
54
+
}
55
+
#Account {
56
+
width: 35%;
57
+
height: 80vh;
58
+
overflow-y: scroll;
59
+
padding: 20px;
60
+
background-color: #070311;
61
+
62
+
border-radius: 10px;
63
+
}
64
+
#Header {
65
+
text-align: center;
66
+
font-size: 2em;
67
+
margin-bottom: 20px;
68
+
}
69
</style>
+20
-46
src/app.css
···
1
-
:root {
2
-
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
-
line-height: 1.5;
4
-
font-weight: 400;
5
6
-
color-scheme: light dark;
7
-
color: rgba(255, 255, 255, 0.87);
8
-
background-color: #242424;
0
9
10
-
font-synthesis: none;
11
-
text-rendering: optimizeLegibility;
12
-
-webkit-font-smoothing: antialiased;
13
-
-moz-osx-font-smoothing: grayscale;
0
0
14
}
15
16
a {
···
28
place-items: center;
29
min-width: 320px;
30
min-height: 100vh;
0
0
0
0
0
31
}
32
33
h1 {
···
35
line-height: 1.1;
36
}
37
38
-
.card {
39
-
padding: 2em;
40
-
}
41
-
42
#app {
43
-
max-width: 1280px;
44
margin: 0 auto;
45
padding: 2rem;
46
text-align: center;
47
}
48
49
-
button {
50
-
border-radius: 8px;
51
-
border: 1px solid transparent;
52
-
padding: 0.6em 1.2em;
53
-
font-size: 1em;
54
-
font-weight: 500;
55
-
font-family: inherit;
56
-
background-color: #1a1a1a;
57
-
cursor: pointer;
58
-
transition: border-color 0.25s;
59
-
}
60
-
button:hover {
61
-
border-color: #646cff;
62
-
}
63
-
button:focus,
64
-
button:focus-visible {
65
-
outline: 4px auto -webkit-focus-ring-color;
66
-
}
67
68
-
@media (prefers-color-scheme: light) {
69
-
:root {
70
-
color: #213547;
71
-
background-color: #ffffff;
72
-
}
73
-
a:hover {
74
-
color: #747bff;
75
-
}
76
-
button {
77
-
background-color: #f9f9f9;
78
-
}
79
-
}
···
1
+
@font-face {
2
+
font-family: 'ProggyClean';
3
+
src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
4
+
}
5
6
+
::-webkit-scrollbar {
7
+
width: 0px;
8
+
background: transparent;
9
+
}
10
11
+
* {
12
+
scrollbar-width: thin;
13
+
scrollbar-color: transparent transparent;
14
+
-ms-overflow-style: none; /* IE and Edge */
15
+
-webkit-overflow-scrolling: touch;
16
+
-webkit-scrollbar: none; /* Safari */
17
}
18
19
a {
···
31
place-items: center;
32
min-width: 320px;
33
min-height: 100vh;
34
+
background-color: #12082b;
35
+
font-family: 'ProggyClean', monospace;
36
+
font-size: 24px;
37
+
color: white;
38
+
border-color: #8054f0;
39
}
40
41
h1 {
···
43
line-height: 1.1;
44
}
45
0
0
0
0
46
#app {
47
+
max-width: 1400px;
48
margin: 0 auto;
49
padding: 2rem;
50
text-align: center;
51
}
52
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
53
0
0
0
0
0
0
0
0
0
0
0
0
+20
-4
src/lib/AccountComponent.svelte
···
1
<script lang="ts">
2
import type { AccountMetadata } from "./pdsfetch";
3
const { account }: { account: AccountMetadata } = $props();
0
4
</script>
5
<div id="accountContainer">
6
{#if account.avatarCid}
7
<img
8
id="avatar"
9
alt="avatar of {account.displayName}"
10
-
src="https://pds.witchcraft.systems/xrpc/com.atproto.sync.getBlob?did={account.did}&cid={account.avatarCid}"
11
/>
12
{/if}
13
-
<p>{account.displayName}</p>
14
</div>
15
<style>
16
#accountContainer {
17
-
display: column;
18
text-align: start;
19
-
border: 2px solid black;
0
20
padding: 4%;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
21
}
22
#avatar {
23
width: 50px;
···
1
<script lang="ts">
2
import type { AccountMetadata } from "./pdsfetch";
3
const { account }: { account: AccountMetadata } = $props();
4
+
import { Config } from "../../config";
5
</script>
6
<div id="accountContainer">
7
{#if account.avatarCid}
8
<img
9
id="avatar"
10
alt="avatar of {account.displayName}"
11
+
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={account.did}&cid={account.avatarCid}"
12
/>
13
{/if}
14
+
<div id="accountName">{account.displayName || account.did}</div>
15
</div>
16
<style>
17
#accountContainer {
18
+
display: flex;
19
text-align: start;
20
+
align-items: center;
21
+
background-color: #0d0620;
22
padding: 4%;
23
+
margin: 10px;
24
+
25
+
/* round corners */
26
+
border-radius: 10px;
27
+
}
28
+
#accountName {
29
+
margin-left: 10px;
30
+
font-size: 0.9em;
31
+
32
+
/* replace overflow with ellipsis */
33
+
overflow: hidden;
34
+
text-overflow: ellipsis;
35
+
white-space: nowrap;
36
+
max-width: 80%;
37
}
38
#avatar {
39
width: 50px;
+51
-10
src/lib/PostComponent.svelte
···
13
alt="avatar of {post.displayName}"
14
/>
15
{/if}
16
-
<p>{post.displayName} | {post.timenotstamp}</p>
17
</div>
18
<div id="postContent">
19
-
<p>{post.text}</p>
20
{#if post.replyingDid}
21
-
<p>Replying to: {post.replyingDid}</p>
22
-
{/if}
0
23
{#if post.imagesCid}
24
<div id="imagesContainer">
25
{#each post.imagesCid as imageLink}
···
42
43
<style>
44
#postContainer {
45
-
display: column;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
46
text-align: start;
47
-
border: 2px solid black;
48
-
padding: 4%;
0
0
0
0
0
0
0
0
0
49
}
50
-
#postHeader {
51
-
text-decoration: underline;
0
0
0
0
0
0
52
}
53
#avatar {
54
width: 50px;
55
height: 50px;
56
-
border-radius: 50%;
0
0
57
}
58
#embedImages {
0
0
0
0
0
0
59
width: 50%;
60
height: 50%;
61
}
···
13
alt="avatar of {post.displayName}"
14
/>
15
{/if}
16
+
<div id="headerText">{post.displayName} | {post.timenotstamp}</div>
17
</div>
18
<div id="postContent">
0
19
{#if post.replyingDid}
20
+
<p id="replyingText">replying to: {post.replyingDid}</p>
21
+
{/if}
22
+
<p id="postText">{post.text}</p>
23
{#if post.imagesCid}
24
<div id="imagesContainer">
25
{#each post.imagesCid as imageLink}
···
42
43
<style>
44
#postContainer {
45
+
display: flex;
46
+
flex-direction: column;
47
+
border: 1px solid #8054f0;
48
+
background-color: black;
49
+
margin-bottom: -1px;
50
+
}
51
+
#postHeader {
52
+
display: flex;
53
+
flex-direction: row;
54
+
align-items: center;
55
+
justify-content: start;
56
+
background-color: #1f1145;
57
+
padding: 0px 0px;
58
+
height: fit-content;
59
+
border-bottom: 1px solid #8054f0;
60
+
font-weight: bold;
61
+
}
62
+
#postContent {
63
+
display: flex;
64
text-align: start;
65
+
flex-direction: column;
66
+
padding: 10px;
67
+
background-color: #0d0620;
68
+
color: white;
69
+
}
70
+
#replyingText {
71
+
font-size: 0.7em;
72
+
color: white;
73
+
margin: 0;
74
+
margin-bottom: 10px;
75
+
padding: 0;
76
}
77
+
#postText {
78
+
margin: 0;
79
+
padding: 0;
80
+
}
81
+
#headerText {
82
+
margin-left: 10px;
83
+
font-size: 0.9em;
84
+
text-align: start;
85
}
86
#avatar {
87
width: 50px;
88
height: 50px;
89
+
margin: 0px;
90
+
margin-left: 0px;
91
+
border-right: #8054f0 1px solid;
92
}
93
#embedImages {
94
+
width: 50%;
95
+
height: 50%;
96
+
margin-top: 0px;
97
+
margin-bottom: -5px;
98
+
}
99
+
#embedVideo {
100
width: 50%;
101
height: 50%;
102
}
+36
-14
src/lib/pdsfetch.ts
···
5
AppBskyFeedPost,
6
ComAtprotoRepoListRecords,
7
} from "@atcute/client/lexicons";
0
8
// import { ComAtprotoRepoListRecords.Record } from "@atcute/client/lexicons";
9
// import { AppBskyFeedPost } from "@atcute/client/lexicons";
10
// import { AppBskyActorDefs } from "@atcute/client/lexicons";
···
87
88
const rpc = new XRPC({
89
handler: simpleFetchHandler({
90
-
service: "https://pds.witchcraft.systems",
91
}),
92
});
93
···
99
};
100
const getAccountMetadata = async (did: `did:${string}:${string}`) => {
101
// gonna assume self exists in the app.bsky.actor.profile
0
102
const { data } = await rpc.get("com.atproto.repo.getRecord", {
103
params: {
104
repo: did,
···
116
account.avatarCid = value.avatar.ref["$link"];
117
}
118
return account;
0
0
0
0
0
0
0
0
0
119
};
120
121
const getAllMetadataFromPds = async () => {
···
125
return await getAccountMetadata(repo);
126
}),
127
);
128
-
return metadata;
129
};
130
131
const fetchPosts = async (did: string) => {
132
-
const { data } = await rpc.get("com.atproto.repo.listRecords", {
133
-
params: {
134
-
repo: did,
135
-
collection: "app.bsky.feed.post",
136
-
limit: 5,
137
-
},
138
-
});
139
-
return {
140
-
records: data.records as ComAtprotoRepoListRecords.Record[],
141
-
did: did,
142
-
};
0
0
0
0
0
0
0
0
0
0
143
};
144
145
const fetchAllPosts = async () => {
···
149
await fetchPosts(metadata.did)
150
),
151
);
152
-
const posts: Post[] = postRecords.flatMap((userFetch) =>
0
153
userFetch.records.map((record) => {
154
const user = users.find((user: AccountMetadata) =>
155
user.did == userFetch.did
···
5
AppBskyFeedPost,
6
ComAtprotoRepoListRecords,
7
} from "@atcute/client/lexicons";
8
+
import { Config } from "../../config";
9
// import { ComAtprotoRepoListRecords.Record } from "@atcute/client/lexicons";
10
// import { AppBskyFeedPost } from "@atcute/client/lexicons";
11
// import { AppBskyActorDefs } from "@atcute/client/lexicons";
···
88
89
const rpc = new XRPC({
90
handler: simpleFetchHandler({
91
+
service: Config.PDS_URL,
92
}),
93
});
94
···
100
};
101
const getAccountMetadata = async (did: `did:${string}:${string}`) => {
102
// gonna assume self exists in the app.bsky.actor.profile
103
+
try {
104
const { data } = await rpc.get("com.atproto.repo.getRecord", {
105
params: {
106
repo: did,
···
118
account.avatarCid = value.avatar.ref["$link"];
119
}
120
return account;
121
+
}
122
+
catch (e) {
123
+
console.error(`Error fetching metadata for ${did}:`, e);
124
+
return {
125
+
did: "error",
126
+
displayName: "",
127
+
avatarCid: null,
128
+
};
129
+
}
130
};
131
132
const getAllMetadataFromPds = async () => {
···
136
return await getAccountMetadata(repo);
137
}),
138
);
139
+
return metadata.filter(account => account.did !== "error");
140
};
141
142
const fetchPosts = async (did: string) => {
143
+
try {
144
+
const { data } = await rpc.get("com.atproto.repo.listRecords", {
145
+
params: {
146
+
repo: did,
147
+
collection: "app.bsky.feed.post",
148
+
limit: 5,
149
+
},
150
+
});
151
+
return {
152
+
records: data.records as ComAtprotoRepoListRecords.Record[],
153
+
did: did,
154
+
error: false
155
+
};
156
+
} catch (e) {
157
+
console.error(`Error fetching posts for ${did}:`, e);
158
+
return {
159
+
records: [],
160
+
did: did,
161
+
error: true
162
+
};
163
+
}
164
};
165
166
const fetchAllPosts = async () => {
···
170
await fetchPosts(metadata.did)
171
),
172
);
173
+
const validPostRecords = postRecords.filter(record => !record.error);
174
+
const posts: Post[] = validPostRecords.flatMap((userFetch) =>
175
userFetch.records.map((record) => {
176
const user = users.find((user: AccountMetadata) =>
177
user.did == userFetch.did