Compare commits
No commits in common. "main" and "SvelteStuff" have entirely different histories.
main
...
SvelteStuf
17 changed files with 229 additions and 773 deletions
|
@ -1,58 +0,0 @@
|
||||||
name: Deploy
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- astra/ci
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
name: Deploy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
|
|
||||||
- name: Setup Deno
|
|
||||||
uses: https://github.com/denoland/setup-deno@v2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: deno install
|
|
||||||
|
|
||||||
- name: Build project
|
|
||||||
run: deno task build
|
|
||||||
|
|
||||||
- name: Setup SSH
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
|
||||||
cat > ~/.ssh/config << EOF
|
|
||||||
Host deploy
|
|
||||||
HostName ${{ vars.SERVER_HOST }}
|
|
||||||
User ${{ vars.SERVER_USER }}
|
|
||||||
IdentityFile ~/.ssh/id_ed25519
|
|
||||||
StrictHostKeyChecking accept-new
|
|
||||||
BatchMode yes
|
|
||||||
PasswordAuthentication no
|
|
||||||
PubkeyAuthentication yes
|
|
||||||
EOF
|
|
||||||
chmod 600 ~/.ssh/config
|
|
||||||
ssh-keyscan -H ${{ vars.SERVER_HOST }} >> ~/.ssh/known_hosts
|
|
||||||
echo "Deploying to ${{ vars.SERVER_HOST }} as ${{ vars.SERVER_USER }} to /var/www/pds/${{ github.ref_name }}"
|
|
||||||
|
|
||||||
- name: Debug SSH Connection
|
|
||||||
run: ssh -v deploy echo "SSH Connection Successful"
|
|
||||||
|
|
||||||
- name: Create folder if not exists
|
|
||||||
run: ssh deploy "mkdir -p /var/www/pds/${{ github.ref_name }}"
|
|
||||||
|
|
||||||
- name: Deploy via SCP
|
|
||||||
run: scp -r ./dist/* deploy:/var/www/pds/${{ github.ref_name }}
|
|
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
||||||
# MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 Witchcraft Systems
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
61
README.md
61
README.md
|
@ -1,62 +1,3 @@
|
||||||
# pds-dash
|
# pds-dash
|
||||||
|
|
||||||
a frontend dashboard with stats for your ATProto PDS.
|
Frontend with stats for your ATProto PDS
|
||||||
|
|
||||||
## setup
|
|
||||||
|
|
||||||
### prerequisites
|
|
||||||
|
|
||||||
- [deno](https://deno.com/manual/getting_started/installation)
|
|
||||||
|
|
||||||
### installing
|
|
||||||
|
|
||||||
clone the repo, install dependencies using deno:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
deno install
|
|
||||||
```
|
|
||||||
|
|
||||||
### development server
|
|
||||||
|
|
||||||
local develompent server with hot reloading:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
deno task dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### building
|
|
||||||
|
|
||||||
to build the optimized bundle run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
deno task build
|
|
||||||
```
|
|
||||||
|
|
||||||
the output will be in the `dist/` directory.
|
|
||||||
|
|
||||||
## deploying
|
|
||||||
|
|
||||||
we use our own CI/CD workflow at [`.forgejo/workflows/deploy.yaml`](.forgejo/workflows/deploy.yaml), but it boils down to building the project bundle and deploying it to a web server. it'll probably make more sense to host it on the same domain as your PDS, but it doesn't affect anything if you host it somewhere else.
|
|
||||||
|
|
||||||
## configuring
|
|
||||||
|
|
||||||
[`config.ts`](config.ts) is the main configuration file, you can find more information in the file itself.
|
|
||||||
|
|
||||||
## theming
|
|
||||||
|
|
||||||
the colors are designated in [`src/app.css`](src/app.css) as variables, go crazy with them
|
|
||||||
|
|
||||||
the rest is done by editing the css files and style tags directly, good luck
|
|
||||||
|
|
||||||
relevant files:
|
|
||||||
|
|
||||||
- [`src/App.svelte`](src/App.svelte)
|
|
||||||
- [`src/app.css`](src/app.css)
|
|
||||||
- [`src/lib/AccountComponent.svelte`](src/lib/AccountComponent.svelte)
|
|
||||||
- [`src/lib/PostComponent.svelte`](src/lib/PostComponent.svelte)
|
|
||||||
|
|
||||||
the favicon is located at [`public/favicon.ico`](public/favicon.ico)
|
|
||||||
|
|
||||||
## license
|
|
||||||
|
|
||||||
MIT
|
|
43
config.ts
43
config.ts
|
@ -2,36 +2,15 @@
|
||||||
* Configuration module for the PDS Dashboard
|
* Configuration module for the PDS Dashboard
|
||||||
*/
|
*/
|
||||||
export class Config {
|
export class Config {
|
||||||
/**
|
/**
|
||||||
* The base URL of the PDS (Personal Data Server)
|
* The base URL of the PDS (Personal Data Server)
|
||||||
* @default "https://pds.witchcraft.systems"
|
* @default "https://pds.witchcraft.systems"
|
||||||
*/
|
*/
|
||||||
static readonly PDS_URL: string = "https://pds.witchcraft.systems";
|
static readonly PDS_URL: string = "https://pds.witchcraft.systems";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base URL of the frontend service for linking to replies/quotes/accounts etc.
|
* The base URL of the frontend service for linking to replies
|
||||||
* @default "https://deer.social"
|
* @default "https://deer.social"
|
||||||
*/
|
*/
|
||||||
static readonly FRONTEND_URL: string = "https://deer.social";
|
static readonly FRONTEND_URL: string = "https://deer.social";
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Maximum number of posts to fetch from the PDS per request
|
|
||||||
* Should be around 20 for about 10 users on the pds
|
|
||||||
* The more users you have, the lower the number should be
|
|
||||||
* since sorting is slow and is done on the frontend
|
|
||||||
* @default 20
|
|
||||||
*/
|
|
||||||
static readonly MAX_POSTS: number = 20;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Footer text for the dashboard, you probably want to change this
|
|
||||||
*/
|
|
||||||
static readonly FOOTER_TEXT: string =
|
|
||||||
"Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a><br><br><a href='https://git.witchcraft.systems/scientific-witchery/pds-dash' target='_blank'>Source</a> (<a href='https://github.com/witchcraft-systems/pds-dash/' target='_blank'>github mirror</a>)";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show the posts that are in the future
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
static readonly SHOW_FUTURE_POSTS: boolean = false;
|
|
||||||
}
|
|
10
deno.lock
generated
10
deno.lock
generated
|
@ -6,9 +6,7 @@
|
||||||
"npm:@atcute/identity-resolver@~0.1.2": "0.1.2_@atcute+identity@0.1.3",
|
"npm:@atcute/identity-resolver@~0.1.2": "0.1.2_@atcute+identity@0.1.3",
|
||||||
"npm:@sveltejs/vite-plugin-svelte@^5.0.3": "5.0.3_svelte@5.28.1__acorn@8.14.1_vite@6.3.2__picomatch@4.0.2",
|
"npm:@sveltejs/vite-plugin-svelte@^5.0.3": "5.0.3_svelte@5.28.1__acorn@8.14.1_vite@6.3.2__picomatch@4.0.2",
|
||||||
"npm:@tsconfig/svelte@^5.0.4": "5.0.4",
|
"npm:@tsconfig/svelte@^5.0.4": "5.0.4",
|
||||||
"npm:moment@^2.30.1": "2.30.1",
|
|
||||||
"npm:svelte-check@^4.1.5": "4.1.6_svelte@5.28.1__acorn@8.14.1_typescript@5.7.3",
|
"npm:svelte-check@^4.1.5": "4.1.6_svelte@5.28.1__acorn@8.14.1_typescript@5.7.3",
|
||||||
"npm:svelte-infinite-loading@^1.4.0": "1.4.0",
|
|
||||||
"npm:svelte@^5.23.1": "5.28.1_acorn@8.14.1",
|
"npm:svelte@^5.23.1": "5.28.1_acorn@8.14.1",
|
||||||
"npm:typescript@~5.7.2": "5.7.3",
|
"npm:typescript@~5.7.2": "5.7.3",
|
||||||
"npm:vite@^6.3.1": "6.3.2_picomatch@4.0.2"
|
"npm:vite@^6.3.1": "6.3.2_picomatch@4.0.2"
|
||||||
|
@ -339,9 +337,6 @@
|
||||||
"@jridgewell/sourcemap-codec"
|
"@jridgewell/sourcemap-codec"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"moment@2.30.1": {
|
|
||||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="
|
|
||||||
},
|
|
||||||
"mri@1.2.0": {
|
"mri@1.2.0": {
|
||||||
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="
|
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="
|
||||||
},
|
},
|
||||||
|
@ -416,9 +411,6 @@
|
||||||
"typescript"
|
"typescript"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"svelte-infinite-loading@1.4.0": {
|
|
||||||
"integrity": "sha512-Jo+f/yr/HmZQuIiiKKzAHVFXdAUWHW2RBbrcQTil8JVk1sCm/riy7KTJVzjBgQvHasrFQYKF84zvtc9/Y4lFYg=="
|
|
||||||
},
|
|
||||||
"svelte@5.28.1_acorn@8.14.1": {
|
"svelte@5.28.1_acorn@8.14.1": {
|
||||||
"integrity": "sha512-iOa9WmfNG95lSOSJdMhdjJ4Afok7IRAQYXpbnxhd5EINnXseG0GVa9j6WPght4eX78XfFez45Fi+uRglGKPV/Q==",
|
"integrity": "sha512-iOa9WmfNG95lSOSJdMhdjJ4Afok7IRAQYXpbnxhd5EINnXseG0GVa9j6WPght4eX78XfFez45Fi+uRglGKPV/Q==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
@ -478,9 +470,7 @@
|
||||||
"npm:@atcute/identity-resolver@~0.1.2",
|
"npm:@atcute/identity-resolver@~0.1.2",
|
||||||
"npm:@sveltejs/vite-plugin-svelte@^5.0.3",
|
"npm:@sveltejs/vite-plugin-svelte@^5.0.3",
|
||||||
"npm:@tsconfig/svelte@^5.0.4",
|
"npm:@tsconfig/svelte@^5.0.4",
|
||||||
"npm:moment@^2.30.1",
|
|
||||||
"npm:svelte-check@^4.1.5",
|
"npm:svelte-check@^4.1.5",
|
||||||
"npm:svelte-infinite-loading@^1.4.0",
|
|
||||||
"npm:svelte@^5.23.1",
|
"npm:svelte@^5.23.1",
|
||||||
"npm:typescript@~5.7.2",
|
"npm:typescript@~5.7.2",
|
||||||
"npm:vite@^6.3.1"
|
"npm:vite@^6.3.1"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|
|
@ -12,9 +12,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atcute/bluesky": "^2.0.2",
|
"@atcute/bluesky": "^2.0.2",
|
||||||
"@atcute/client": "^3.0.1",
|
"@atcute/client": "^3.0.1",
|
||||||
"@atcute/identity-resolver": "^0.1.2",
|
"@atcute/identity-resolver": "^0.1.2"
|
||||||
"moment": "^2.30.1",
|
|
||||||
"svelte-infinite-loading": "^1.4.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
|
|
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
138
src/App.svelte
138
src/App.svelte
|
@ -1,71 +1,40 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PostComponent from "./lib/PostComponent.svelte";
|
import PostComponent from "./lib/PostComponent.svelte";
|
||||||
import AccountComponent from "./lib/AccountComponent.svelte";
|
import AccountComponent from "./lib/AccountComponent.svelte";
|
||||||
import InfiniteLoading from "svelte-infinite-loading";
|
import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
|
||||||
import { getNextPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
|
const postsPromise = fetchAllPosts();
|
||||||
import { Config } from "../config";
|
|
||||||
const accountsPromise = getAllMetadataFromPds();
|
const accountsPromise = getAllMetadataFromPds();
|
||||||
import { onMount } from "svelte";
|
|
||||||
|
|
||||||
let posts: Post[] = [];
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
// Fetch initial posts
|
|
||||||
getNextPosts().then((initialPosts) => {
|
|
||||||
posts = initialPosts;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Infinite loading function
|
|
||||||
const onInfinite = ({
|
|
||||||
detail: { loaded, complete },
|
|
||||||
}: {
|
|
||||||
detail: { loaded: () => void; complete: () => void };
|
|
||||||
}) => {
|
|
||||||
getNextPosts().then((newPosts) => {
|
|
||||||
console.log("Loading next posts...");
|
|
||||||
if (newPosts.length > 0) {
|
|
||||||
posts = [...posts, ...newPosts];
|
|
||||||
loaded();
|
|
||||||
} else {
|
|
||||||
complete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div id="Content">
|
<div id="Content">
|
||||||
{#await accountsPromise}
|
{#await accountsPromise}
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
{:then accountsData}
|
{:then accountsData}
|
||||||
<div id="Account">
|
<div id="Account">
|
||||||
<h1 id="Header">ATProto PDS</h1>
|
<h1 id="Header">ATProto PDS</h1>
|
||||||
<p>Home to {accountsData.length} accounts</p>
|
<p>Home to {accountsData.length} accounts</p>
|
||||||
<div id="accountsList">
|
{#each accountsData as accountObject}
|
||||||
{#each accountsData as accountObject}
|
<AccountComponent account={accountObject} />
|
||||||
<AccountComponent account={accountObject} />
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
{:catch error}
|
||||||
<p>{@html Config.FOOTER_TEXT}</p>
|
<p>Error: {error.message}</p>
|
||||||
</div>
|
{/await}
|
||||||
{:catch error}
|
|
||||||
<p>Error: {error.message}</p>
|
|
||||||
{/await}
|
|
||||||
|
|
||||||
|
{#await postsPromise}
|
||||||
|
<p>Loading...</p>
|
||||||
|
{:then postsData}
|
||||||
<div id="Feed">
|
<div id="Feed">
|
||||||
<div id="spacer"></div>
|
{#each postsData as postObject}
|
||||||
{#each posts as postObject}
|
|
||||||
<PostComponent post={postObject as Post} />
|
<PostComponent post={postObject as Post} />
|
||||||
{/each}
|
{/each}
|
||||||
<InfiniteLoading on:infinite={onInfinite} distance={3000} />
|
|
||||||
<div id="spacer"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* desktop style */
|
|
||||||
|
|
||||||
#Content {
|
#Content {
|
||||||
display: flex;
|
display: flex;
|
||||||
/* split the screen in half, left for accounts, right for posts */
|
/* split the screen in half, left for accounts, right for posts */
|
||||||
|
@ -74,80 +43,27 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--background-color);
|
background-color: #12082b;
|
||||||
color: var(--text-color);
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
#Feed {
|
#Feed {
|
||||||
width: 65%;
|
width: 65%;
|
||||||
height: 100vh;
|
height: 80vh;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
padding-bottom: 0;
|
|
||||||
padding-top: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
#spacer {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
height: 10vh;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
#Account {
|
#Account {
|
||||||
width: 35%;
|
width: 35%;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
background-color: var(--content-background-color);
|
|
||||||
height: 80vh;
|
height: 80vh;
|
||||||
padding: 20px;
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
#accountsList {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
height: 100%;
|
padding: 20px;
|
||||||
width: 100%;
|
background-color: #070311;
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
#Header {
|
#Header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* mobile style */
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
#Content {
|
|
||||||
flex-direction: column;
|
|
||||||
width: auto;
|
|
||||||
padding-left: 0px;
|
|
||||||
padding-right: 0px;
|
|
||||||
margin-top: 5%;
|
|
||||||
}
|
|
||||||
#Account {
|
|
||||||
width: auto;
|
|
||||||
padding-left: 5%;
|
|
||||||
padding-right: 5%;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
margin-left: 5%;
|
|
||||||
margin-right: 5%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
#Feed {
|
|
||||||
width: 95%;
|
|
||||||
margin: 0px;
|
|
||||||
margin-left: 10%;
|
|
||||||
margin-right: 10%;
|
|
||||||
padding: 0px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#spacer {
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
56
src/app.css
56
src/app.css
|
@ -1,44 +1,15 @@
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "ProggyClean";
|
font-family: 'ProggyClean';
|
||||||
src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
|
src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
|
||||||
--link-color: #646cff;
|
|
||||||
--link-hover-color: #535bf2;
|
|
||||||
--background-color: #12082b;
|
|
||||||
--header-background-color: #1f1145;
|
|
||||||
--content-background-color: #0d0620;
|
|
||||||
--text-color: white;
|
|
||||||
--border-color: #8054f0;
|
|
||||||
--indicator-inactive-color: #4a4a4a;
|
|
||||||
--indicator-active-color: #8054f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 0px;
|
width: 0px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-corner {
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-button {
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
scrollbar-width: none;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: transparent transparent;
|
scrollbar-color: transparent transparent;
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
|
@ -47,12 +18,11 @@
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--link-color);
|
color: #646cff;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
color: var(--link-hover-color);
|
color: #535bf2;
|
||||||
text-decoration: underline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -61,11 +31,11 @@ body {
|
||||||
place-items: center;
|
place-items: center;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: var(--background-color);
|
background-color: #12082b;
|
||||||
font-family: "ProggyClean", monospace;
|
font-family: 'ProggyClean', monospace;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: var(--text-color);
|
color: white;
|
||||||
border-color: var(--border-color);
|
border-color: #8054f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
@ -75,9 +45,9 @@ h1 {
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin: 0;
|
margin: 0 auto;
|
||||||
padding: 0;
|
padding: 2rem;
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
1
src/assets/svelte.svg
Normal file
1
src/assets/svelte.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -1,48 +1,49 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { AccountMetadata } from "./pdsfetch";
|
import type { AccountMetadata } from "./pdsfetch";
|
||||||
const { account }: { account: AccountMetadata } = $props();
|
const { account }: { account: AccountMetadata } = $props();
|
||||||
import { Config } from "../../config";
|
import { Config } from "../../config";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a id="link" href="{Config.FRONTEND_URL}/profile/{account.did}">
|
<a id="link" href="{Config.FRONTEND_URL}/profile/{account.did}">
|
||||||
<div id="accountContainer">
|
<div id="accountContainer">
|
||||||
{#if account.avatarCid}
|
{#if account.avatarCid}
|
||||||
<img
|
<img
|
||||||
id="avatar"
|
id="avatar"
|
||||||
alt="avatar of {account.displayName}"
|
alt="avatar of {account.displayName}"
|
||||||
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={account.did}&cid={account.avatarCid}"
|
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={account.did}&cid={account.avatarCid}"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div id="accountName">
|
<div id="accountName">
|
||||||
{account.displayName || account.handle || account.did}
|
{account.displayName || account.handle || account.did}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#accountContainer {
|
#accountContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--background-color);
|
background-color: #0d0620;
|
||||||
padding: 0px;
|
padding: 4%;
|
||||||
margin-bottom: 15px;
|
margin: 10px;
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
#accountName {
|
|
||||||
margin-left: 10px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
|
|
||||||
/* replace overflow with ellipsis */
|
/* round corners */
|
||||||
overflow: hidden;
|
border-radius: 10px;
|
||||||
text-overflow: ellipsis;
|
}
|
||||||
white-space: nowrap;
|
#accountName {
|
||||||
max-width: 80%;
|
margin-left: 10px;
|
||||||
}
|
font-size: 0.9em;
|
||||||
#avatar {
|
|
||||||
width: 50px;
|
/* replace overflow with ellipsis */
|
||||||
height: 50px;
|
overflow: hidden;
|
||||||
margin: 0px;
|
text-overflow: ellipsis;
|
||||||
border-right: var(--border-color) 1px solid;
|
white-space: nowrap;
|
||||||
}
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
#avatar {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,59 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Post } from "./pdsfetch";
|
import { Post } from "./pdsfetch";
|
||||||
import { Config } from "../../config";
|
import { Config } from "../../config";
|
||||||
import { onMount } from "svelte";
|
|
||||||
import moment from "moment";
|
|
||||||
|
|
||||||
let { post }: { post: Post } = $props();
|
let { post }: { post: Post } = $props();
|
||||||
|
|
||||||
// State for image carousel
|
|
||||||
let currentImageIndex = $state(0);
|
|
||||||
|
|
||||||
// Functions to navigate carousel
|
|
||||||
function nextImage() {
|
|
||||||
if (post.imagesCid && currentImageIndex < post.imagesCid.length - 1) {
|
|
||||||
currentImageIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function prevImage() {
|
|
||||||
if (currentImageIndex > 0) {
|
|
||||||
currentImageIndex--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to preload an image
|
|
||||||
function preloadImage(index: number): void {
|
|
||||||
if (!post.imagesCid || index < 0 || index >= post.imagesCid.length) return;
|
|
||||||
|
|
||||||
const img = new Image();
|
|
||||||
img.src = `${Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did=${post.authorDid}&cid=${post.imagesCid[index]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preload adjacent images when current index changes
|
|
||||||
$effect(() => {
|
|
||||||
if (post.imagesCid && post.imagesCid.length > 1) {
|
|
||||||
// Preload next image if available
|
|
||||||
if (currentImageIndex < post.imagesCid.length - 1) {
|
|
||||||
preloadImage(currentImageIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preload previous image if available
|
|
||||||
if (currentImageIndex > 0) {
|
|
||||||
preloadImage(currentImageIndex - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initial preload of images
|
|
||||||
onMount(() => {
|
|
||||||
if (post.imagesCid && post.imagesCid.length > 1) {
|
|
||||||
// Preload the next image if it exists
|
|
||||||
if (post.imagesCid.length > 1) {
|
|
||||||
preloadImage(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="postContainer">
|
<div id="postContainer">
|
||||||
|
@ -66,229 +14,110 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div id="headerText">
|
<div id="headerText">
|
||||||
<a id="displayName" href="{Config.FRONTEND_URL}/profile/{post.authorDid}"
|
<a href="{Config.FRONTEND_URL}/profile/{post.authorDid}"
|
||||||
>{post.displayName}</a
|
>{post.displayName} ( {post.authorHandle} )</a
|
||||||
|
>
|
||||||
|
|
|
||||||
|
<a href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.cid}"
|
||||||
|
>{post.timenotstamp}</a
|
||||||
>
|
>
|
||||||
<p id="handle">
|
|
||||||
<a href="{Config.FRONTEND_URL}/profile/{post.authorHandle}"
|
|
||||||
>{post.authorHandle}</a
|
|
||||||
>
|
|
||||||
|
|
||||||
<a
|
|
||||||
id="postLink"
|
|
||||||
href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.recordName}"
|
|
||||||
>{moment(post.timenotstamp).isBefore(moment().subtract(1, "month"))
|
|
||||||
? moment(post.timenotstamp).format("MMM D, YYYY")
|
|
||||||
: moment(post.timenotstamp).fromNow()}</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="postContent">
|
<div id="postContent">
|
||||||
{#if post.replyingUri}
|
{#if post.replyingUri}
|
||||||
<a
|
<a
|
||||||
id="replyingText"
|
id="replyingText"
|
||||||
href="{Config.FRONTEND_URL}/profile/{post.replyingUri.repo}/post/{post
|
href="https://deer.social/profile/{post.replyingUri.repo}/post/{post
|
||||||
.replyingUri.rkey}">replying to {post.replyingUri.repo}</a
|
.replyingUri.rkey}">replying to {post.replyingUri.repo}</a
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
<p>{post.text}</p>
|
||||||
|
|
||||||
{#if post.quotingUri}
|
{#if post.quotingUri}
|
||||||
<a
|
<a
|
||||||
id="quotingText"
|
id="quotingText"
|
||||||
href="{Config.FRONTEND_URL}/profile/{post.quotingUri.repo}/post/{post
|
href="https://deer.social/profile/{post.quotingUri.repo}/post/{post
|
||||||
.quotingUri.rkey}">quoting {post.quotingUri.repo}</a
|
.quotingUri.rkey}">quoting {post.quotingUri.repo}</a
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<div id="postText">{post.text}</div>
|
{#if post.imagesCid}
|
||||||
{#if post.imagesCid && post.imagesCid.length > 0}
|
<div id="imagesContainer">
|
||||||
<div id="carouselContainer">
|
{#each post.imagesCid as imageLink}
|
||||||
<img
|
<img
|
||||||
id="embedImages"
|
id="embedImages"
|
||||||
alt="Post Image {currentImageIndex + 1} of {post.imagesCid.length}"
|
alt="Post Image"
|
||||||
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post
|
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={imageLink}"
|
||||||
.imagesCid[currentImageIndex]}"
|
/>
|
||||||
/>
|
{/each}
|
||||||
|
|
||||||
{#if post.imagesCid.length > 1}
|
|
||||||
<div id="carouselControls">
|
|
||||||
<button
|
|
||||||
id="prevBtn"
|
|
||||||
onclick={prevImage}
|
|
||||||
disabled={currentImageIndex === 0}>←</button
|
|
||||||
>
|
|
||||||
<div id="carouselIndicators">
|
|
||||||
{#each post.imagesCid as _, i}
|
|
||||||
<div
|
|
||||||
class="indicator {i === currentImageIndex ? 'active' : ''}"
|
|
||||||
></div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
id="nextBtn"
|
|
||||||
onclick={nextImage}
|
|
||||||
disabled={currentImageIndex === post.imagesCid.length - 1}
|
|
||||||
>→</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if post.videosLinkCid}
|
{#if post.videosLinkCid}
|
||||||
<!-- svelte-ignore a11y_media_has_caption -->
|
|
||||||
<video
|
<video
|
||||||
id="embedVideo"
|
id="embedVideo"
|
||||||
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.videosLinkCid}"
|
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.videosLinkCid}"
|
||||||
controls
|
/>
|
||||||
></video>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
#postContainer {
|
#postContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid #8054f0;
|
||||||
background-color: var(--background-color);
|
background-color: black;
|
||||||
margin-bottom: 15px;
|
margin-bottom: -1px;
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
}
|
||||||
#postHeader {
|
#postHeader {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
background-color: var(--header-background-color);
|
background-color: #1f1145;
|
||||||
padding: 0px 0px;
|
padding: 0px 0px;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid #8054f0;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
overflow-wrap: break-word;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
#displayName {
|
|
||||||
color: var(--text-color);
|
|
||||||
font-size: 1.2em;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
#handle {
|
|
||||||
color: var(--border-color);
|
|
||||||
font-size: 0.8em;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#postLink {
|
|
||||||
color: var(--border-color);
|
|
||||||
font-size: 0.8em;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
#postContent {
|
#postContent {
|
||||||
display: flex;
|
display: flex;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: var(--content-background-color);
|
background-color: #0d0620;
|
||||||
color: var(--text-color);
|
color: white;
|
||||||
overflow-wrap: break-word;
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
}
|
||||||
#replyingText {
|
#replyingText {
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
|
color: white;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
#quotingText {
|
|
||||||
font-size: 0.7em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
}
|
||||||
#postText {
|
#postText {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-wrap: break-word;
|
|
||||||
word-wrap: normal;
|
|
||||||
word-break: break-word;
|
|
||||||
hyphens: none;
|
|
||||||
}
|
}
|
||||||
#headerText {
|
#headerText {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
overflow-wrap: break-word;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
#avatar {
|
#avatar {
|
||||||
height: 100%;
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
border-right: var(--border-color) 1px solid;
|
border-right: #8054f0 1px solid;
|
||||||
}
|
}
|
||||||
#carouselContainer {
|
#embedImages {
|
||||||
position: relative;
|
width: 50%;
|
||||||
width: 100%;
|
height: 50%;
|
||||||
margin-top: 10px;
|
margin-top: 0px;
|
||||||
display: flex;
|
margin-bottom: -5px;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
#carouselControls {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
#carouselIndicators {
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
.indicator {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
background-color: var(--indicator-inactive-color);
|
|
||||||
}
|
|
||||||
.indicator.active {
|
|
||||||
background-color: var(--indicator-active-color);
|
|
||||||
}
|
|
||||||
#prevBtn,
|
|
||||||
#nextBtn {
|
|
||||||
background-color: rgba(31, 17, 69, 0.7);
|
|
||||||
color: var(--text-color);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
#prevBtn:disabled,
|
|
||||||
#nextBtn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
}
|
||||||
#embedVideo {
|
#embedVideo {
|
||||||
width: 100%;
|
width: 50%;
|
||||||
max-width: 500px;
|
height: 50%;
|
||||||
margin-top: 10px;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#embedImages {
|
|
||||||
min-width: min(100%, 500px);
|
|
||||||
max-width: min(100%, 500px);
|
|
||||||
max-height: 500px;
|
|
||||||
object-fit: contain;
|
|
||||||
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,15 +18,11 @@ import { Config } from "../../config";
|
||||||
// import { AppBskyActorDefs } from "@atcute/client/lexicons";
|
// import { AppBskyActorDefs } from "@atcute/client/lexicons";
|
||||||
|
|
||||||
interface AccountMetadata {
|
interface AccountMetadata {
|
||||||
did: At.Did;
|
did: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
handle: string;
|
handle: string;
|
||||||
avatarCid: string | null;
|
avatarCid: string | null;
|
||||||
currentCursor?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let accountsMetadata: AccountMetadata[] = [];
|
|
||||||
|
|
||||||
interface atUriObject {
|
interface atUriObject {
|
||||||
repo: string;
|
repo: string;
|
||||||
collection: string;
|
collection: string;
|
||||||
|
@ -36,7 +32,6 @@ class Post {
|
||||||
authorDid: string;
|
authorDid: string;
|
||||||
authorAvatarCid: string | null;
|
authorAvatarCid: string | null;
|
||||||
postCid: string;
|
postCid: string;
|
||||||
recordName: string;
|
|
||||||
authorHandle: string;
|
authorHandle: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
@ -52,7 +47,6 @@ class Post {
|
||||||
account: AccountMetadata,
|
account: AccountMetadata,
|
||||||
) {
|
) {
|
||||||
this.postCid = record.cid;
|
this.postCid = record.cid;
|
||||||
this.recordName = processAtUri(record.uri).rkey;
|
|
||||||
this.authorDid = account.did;
|
this.authorDid = account.did;
|
||||||
this.authorAvatarCid = account.avatarCid;
|
this.authorAvatarCid = account.avatarCid;
|
||||||
this.authorHandle = account.handle;
|
this.authorHandle = account.handle;
|
||||||
|
@ -71,8 +65,8 @@ class Post {
|
||||||
this.videosLinkCid = null;
|
this.videosLinkCid = null;
|
||||||
switch (post.embed?.$type) {
|
switch (post.embed?.$type) {
|
||||||
case "app.bsky.embed.images":
|
case "app.bsky.embed.images":
|
||||||
this.imagesCid = post.embed.images.map(
|
this.imagesCid = post.embed.images.map((imageRecord: any) =>
|
||||||
(imageRecord: any) => imageRecord.image.ref.$link,
|
imageRecord.image.ref.$link
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "app.bsky.embed.video":
|
case "app.bsky.embed.video":
|
||||||
|
@ -85,8 +79,8 @@ class Post {
|
||||||
this.quotingUri = processAtUri(post.embed.record.record.uri);
|
this.quotingUri = processAtUri(post.embed.record.record.uri);
|
||||||
switch (post.embed.media.$type) {
|
switch (post.embed.media.$type) {
|
||||||
case "app.bsky.embed.images":
|
case "app.bsky.embed.images":
|
||||||
this.imagesCid = post.embed.media.images.map(
|
this.imagesCid = post.embed.media.images.map((imageRecord) =>
|
||||||
(imageRecord) => imageRecord.image.ref.$link,
|
imageRecord.image.ref.$link
|
||||||
);
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -115,50 +109,77 @@ const rpc = new XRPC({
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getDidsFromPDS = async (): Promise<At.Did[]> => {
|
const getDidsFromPDS = async () => {
|
||||||
const { data } = await rpc.get("com.atproto.sync.listRepos", {
|
const { data } = await rpc.get("com.atproto.sync.listRepos", {
|
||||||
params: {},
|
params: {},
|
||||||
});
|
});
|
||||||
return data.repos.map((repo: any) => repo.did) as At.Did[];
|
return data.repos.map((repo: any) => (repo.did));
|
||||||
};
|
};
|
||||||
const getAccountMetadata = async (
|
const getAccountMetadata = async (did: `did:${string}:${string}`) => {
|
||||||
did: `did:${string}:${string}`,
|
|
||||||
) => {
|
|
||||||
// gonna assume self exists in the app.bsky.actor.profile
|
// gonna assume self exists in the app.bsky.actor.profile
|
||||||
try {
|
try {
|
||||||
const { data } = await rpc.get("com.atproto.repo.getRecord", {
|
const { data } = await rpc.get("com.atproto.repo.getRecord", {
|
||||||
params: {
|
params: {
|
||||||
repo: did,
|
repo: did,
|
||||||
collection: "app.bsky.actor.profile",
|
collection: "app.bsky.actor.profile",
|
||||||
rkey: "self",
|
rkey: "self",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const value = data.value as AppBskyActorProfile.Record;
|
const value = data.value as AppBskyActorProfile.Record;
|
||||||
const handle = await blueskyHandleFromDid(did);
|
const handle = await blueskyHandleFromDid(did);
|
||||||
const account: AccountMetadata = {
|
const account: AccountMetadata = {
|
||||||
did: did,
|
did: did,
|
||||||
handle: handle,
|
handle: handle,
|
||||||
displayName: value.displayName || "",
|
displayName: value.displayName || "",
|
||||||
|
avatarCid: null,
|
||||||
|
};
|
||||||
|
if (value.avatar) {
|
||||||
|
account.avatarCid = value.avatar.ref["$link"];
|
||||||
|
}
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(`Error fetching metadata for ${did}:`, e);
|
||||||
|
return {
|
||||||
|
did: "error",
|
||||||
|
displayName: "",
|
||||||
avatarCid: null,
|
avatarCid: null,
|
||||||
};
|
};
|
||||||
if (value.avatar) {
|
|
||||||
account.avatarCid = value.avatar.ref["$link"];
|
|
||||||
}
|
|
||||||
return account;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Error fetching metadata for ${did}:`, e);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAllMetadataFromPds = async (): Promise<AccountMetadata[]> => {
|
const getAllMetadataFromPds = async () => {
|
||||||
const dids = await getDidsFromPDS();
|
const dids = await getDidsFromPDS();
|
||||||
const metadata = await Promise.all(
|
const metadata = await Promise.all(
|
||||||
dids.map(async (repo: `did:${string}:${string}`) => {
|
dids.map(async (repo: `did:${string}:${string}`) => {
|
||||||
return await getAccountMetadata(repo);
|
return await getAccountMetadata(repo);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return metadata.filter((account) => account !== null) as AccountMetadata[];
|
return metadata.filter(account => account.did !== "error");
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchPosts = async (did: string) => {
|
||||||
|
try {
|
||||||
|
const { data } = await rpc.get("com.atproto.repo.listRecords", {
|
||||||
|
params: {
|
||||||
|
repo: did as At.Identifier,
|
||||||
|
collection: "app.bsky.feed.post",
|
||||||
|
limit: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
records: data.records as ComAtprotoRepoListRecords.Record[],
|
||||||
|
did: did,
|
||||||
|
error: false
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error fetching posts for ${did}:`, e);
|
||||||
|
return {
|
||||||
|
records: [],
|
||||||
|
did: did,
|
||||||
|
error: true
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const identityResolve = async (did: At.Did) => {
|
const identityResolve = async (did: At.Did) => {
|
||||||
|
@ -194,146 +215,34 @@ const blueskyHandleFromDid = async (did: At.Did) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
interface PostsAcc {
|
const fetchAllPosts = async () => {
|
||||||
posts: ComAtprotoRepoListRecords.Record[];
|
const users: AccountMetadata[] = await getAllMetadataFromPds();
|
||||||
account: AccountMetadata;
|
const postRecords = await Promise.all(
|
||||||
}
|
users.map(async (metadata: AccountMetadata) =>
|
||||||
const getCutoffDate = (postAccounts: PostsAcc[]) => {
|
await fetchPosts(metadata.did)
|
||||||
const now = Date.now();
|
),
|
||||||
let cutoffDate: Date | null = null;
|
);
|
||||||
postAccounts.forEach((postAcc) => {
|
const validPostRecords = postRecords.filter(record => !record.error);
|
||||||
const latestPost = new Date(
|
const posts: Post[] = validPostRecords.flatMap((userFetch) =>
|
||||||
(postAcc.posts[postAcc.posts.length - 1].value as AppBskyFeedPost.Record)
|
userFetch.records.map((record) => {
|
||||||
.createdAt,
|
const user = users.find((user: AccountMetadata) =>
|
||||||
);
|
user.did == userFetch.did
|
||||||
if (!cutoffDate) {
|
);
|
||||||
cutoffDate = latestPost;
|
if (!user) {
|
||||||
} else {
|
throw new Error(`User with DID ${userFetch.did} not found`);
|
||||||
if (latestPost > cutoffDate) {
|
|
||||||
cutoffDate = latestPost;
|
|
||||||
}
|
}
|
||||||
}
|
return new Post(record, user);
|
||||||
});
|
})
|
||||||
if (cutoffDate) {
|
|
||||||
return cutoffDate;
|
|
||||||
} else {
|
|
||||||
return new Date(now);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterPostsByDate = (posts: PostsAcc[], cutoffDate: Date) => {
|
|
||||||
// filter posts for each account that are older than the cutoff date and save the cursor of the last post included
|
|
||||||
const filteredPosts: PostsAcc[] = posts.map((postAcc) => {
|
|
||||||
const filtered = postAcc.posts.filter((post) => {
|
|
||||||
const postDate = new Date(
|
|
||||||
(post.value as AppBskyFeedPost.Record).createdAt,
|
|
||||||
);
|
|
||||||
return postDate >= cutoffDate;
|
|
||||||
});
|
|
||||||
if (filtered.length > 0) {
|
|
||||||
postAcc.account.currentCursor = processAtUri(filtered[filtered.length - 1].uri).rkey;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
posts: filtered,
|
|
||||||
account: postAcc.account,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return filteredPosts;
|
|
||||||
};
|
|
||||||
// nightmare function. However it works so I am not touching it
|
|
||||||
const getNextPosts = async () => {
|
|
||||||
if (!accountsMetadata.length) {
|
|
||||||
accountsMetadata = await getAllMetadataFromPds();
|
|
||||||
}
|
|
||||||
|
|
||||||
const postsAcc: PostsAcc[] = await Promise.all(
|
|
||||||
accountsMetadata.map(async (account) => {
|
|
||||||
const posts = await fetchPostsForUser(
|
|
||||||
account.did,
|
|
||||||
account.currentCursor || null,
|
|
||||||
);
|
|
||||||
if (posts) {
|
|
||||||
return {
|
|
||||||
posts: posts,
|
|
||||||
account: account,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
posts: [],
|
|
||||||
account: account,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
const recordsFiltered = postsAcc.filter((postAcc) =>
|
posts.sort((a, b) => b.timestamp - a.timestamp);
|
||||||
postAcc.posts.length > 0
|
return posts;
|
||||||
);
|
|
||||||
const cutoffDate = getCutoffDate(recordsFiltered);
|
|
||||||
const recordsCutoff = filterPostsByDate(recordsFiltered, cutoffDate);
|
|
||||||
// update the accountMetadata with the new cursor
|
|
||||||
accountsMetadata = accountsMetadata.map((account) => {
|
|
||||||
const postAcc = recordsCutoff.find(
|
|
||||||
(postAcc) => postAcc.account.did == account.did,
|
|
||||||
);
|
|
||||||
if (postAcc) {
|
|
||||||
account.currentCursor = postAcc.account.currentCursor;
|
|
||||||
}
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// throw the records in a big single array
|
|
||||||
let records = recordsCutoff.flatMap((postAcc) => postAcc.posts);
|
|
||||||
// sort the records by timestamp
|
|
||||||
records = records.sort((a, b) => {
|
|
||||||
const aDate = new Date(
|
|
||||||
(a.value as AppBskyFeedPost.Record).createdAt,
|
|
||||||
).getTime();
|
|
||||||
const bDate = new Date(
|
|
||||||
(b.value as AppBskyFeedPost.Record).createdAt,
|
|
||||||
).getTime();
|
|
||||||
return bDate - aDate;
|
|
||||||
});
|
|
||||||
// filter out posts that are in the future
|
|
||||||
if (!Config.SHOW_FUTURE_POSTS) {
|
|
||||||
const now = Date.now();
|
|
||||||
records = records.filter((post) => {
|
|
||||||
const postDate = new Date(
|
|
||||||
(post.value as AppBskyFeedPost.Record).createdAt,
|
|
||||||
).getTime();
|
|
||||||
return postDate <= now;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPosts = records.map((record) => {
|
|
||||||
const account = accountsMetadata.find(
|
|
||||||
(account) => account.did == processAtUri(record.uri).repo,
|
|
||||||
);
|
|
||||||
if (!account) {
|
|
||||||
throw new Error(
|
|
||||||
`Account with DID ${processAtUri(record.uri).repo} not found`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return new Post(record, account);
|
|
||||||
});
|
|
||||||
return newPosts;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPostsForUser = async (did: At.Did, cursor: string | null) => {
|
const testApiCall = async () => {
|
||||||
try {
|
const { data } = await rpc.get("com.atproto.sync.listRepos", {
|
||||||
const { data } = await rpc.get("com.atproto.repo.listRecords", {
|
params: {},
|
||||||
params: {
|
});
|
||||||
repo: did as At.Identifier,
|
console.log(data);
|
||||||
collection: "app.bsky.feed.post",
|
|
||||||
limit: Config.MAX_POSTS,
|
|
||||||
cursor: cursor || undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return data.records as ComAtprotoRepoListRecords.Record[];
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Error fetching posts for ${did}:`, e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
export { fetchAllPosts, getAllMetadataFromPds, Post };
|
||||||
export { getAllMetadataFromPds, getNextPosts, Post };
|
|
||||||
export type { AccountMetadata };
|
export type { AccountMetadata };
|
||||||
|
|
12
src/main.ts
12
src/main.ts
|
@ -1,9 +1,9 @@
|
||||||
import { mount } from "svelte";
|
import { mount } from 'svelte'
|
||||||
import "./app.css";
|
import './app.css'
|
||||||
import App from "./App.svelte";
|
import App from './App.svelte'
|
||||||
|
|
||||||
const app = mount(App, {
|
const app = mount(App, {
|
||||||
target: document.getElementById("app")!,
|
target: document.getElementById('app')!,
|
||||||
});
|
})
|
||||||
|
|
||||||
export default app;
|
export default app
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||||
// for more information about preprocessors
|
// for more information about preprocessors
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from 'vite'
|
||||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [svelte()],
|
plugins: [svelte()],
|
||||||
});
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue