"Final-ish" design

This commit is contained in:
Astra 2025-04-20 20:08:13 +09:00
parent 037dea4e40
commit ebca274e5a
Signed by: astra
SSH key fingerprint: SHA256:jQDNS75/33T59Ey4yAzrUPP/5YQaXEetsW8hwUae+ag
5 changed files with 202 additions and 33 deletions

View file

@ -6,7 +6,7 @@ export class Config {
* The base URL of the PDS (Personal Data Server)
* @default "https://pds.witchcraft.systems"
*/
static readonly PDS_URL: string = "https://ap.brid.gy";
static readonly PDS_URL: string = "https://pds.witchcraft.systems";
/**
* The base URL of the frontend service for linking to replies
@ -18,5 +18,11 @@ export class Config {
* Maximum number of posts to fetch from the PDS per user
* @default 10
*/
static readonly MAX_POSTS_PER_USER: number = 1;
static readonly MAX_POSTS_PER_USER: number = 22;
/**
* Footer text for the dashboard
* @default "Astrally projected from witchcraft.systems"
*/
static readonly FOOTER_TEXT: string = "Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a>";
}

View file

@ -2,6 +2,7 @@
import PostComponent from "./lib/PostComponent.svelte";
import AccountComponent from "./lib/AccountComponent.svelte";
import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
import { Config } from "../config";
const postsPromise = fetchAllPosts();
const accountsPromise = getAllMetadataFromPds();
</script>
@ -19,6 +20,7 @@
<AccountComponent account={accountObject} />
{/each}
</div>
<p>{@html Config.FOOTER_TEXT}</p>
</div>
{:catch error}
<p>Error: {error.message}</p>
@ -74,6 +76,7 @@
background-color: #0d0620;
height: 80vh;
padding: 20px;
margin-left: 20px;
}
#accountsList {
display: flex;

View file

@ -40,6 +40,7 @@ a {
}
a:hover {
color: #535bf2;
text-decoration: underline;
}
body {

View file

@ -1,7 +1,58 @@
<script lang="ts">
import { Post } from "./pdsfetch";
import { Config } from "../../config";
import { onMount } from "svelte";
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>
<div id="postContainer">
@ -14,13 +65,20 @@
/>
{/if}
<div id="headerText">
<a href="{Config.FRONTEND_URL}/profile/{post.authorDid}"
>{post.displayName} ( {post.authorHandle} )</a
<a id="displayName" href="{Config.FRONTEND_URL}/profile/{post.authorDid}"
>{post.displayName}</a
>
|
<a href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.cid}"
<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}"
>{post.timenotstamp}</a
>
</p>
</div>
</div>
<div id="postContent">
@ -31,7 +89,6 @@
.replyingUri.rkey}">replying to {post.replyingUri.repo}</a
>
{/if}
<div id="postText">{post.text}</div>
{#if post.quotingUri}
<a
id="quotingText"
@ -39,27 +96,54 @@
.quotingUri.rkey}">quoting {post.quotingUri.repo}</a
>
{/if}
{#if post.imagesCid}
<div id="imagesContainer">
{#each post.imagesCid as imageLink}
<div id="postText">{post.text}</div>
{#if post.imagesCid && post.imagesCid.length > 0}
<div id="carouselContainer">
<img
id="embedImages"
alt="Post Image"
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={imageLink}"
alt="Post Image {currentImageIndex + 1} of {post.imagesCid.length}"
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post
.imagesCid[currentImageIndex]}"
/>
{#if post.imagesCid.length > 1}
<div id="carouselControls">
<button
id="prevBtn"
on:click={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"
on:click={nextImage}
disabled={currentImageIndex === post.imagesCid.length - 1}
>→</button
>
</div>
{/if}
</div>
{/if}
{#if post.videosLinkCid}
<video
id="embedVideo"
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.videosLinkCid}"
/>
controls
></video>
{/if}
</div>
</div>
<style>
a:hover {
text-decoration: underline;
}
#postContainer {
display: flex;
flex-direction: column;
@ -79,6 +163,26 @@
border-bottom: 1px solid #8054f0;
font-weight: bold;
overflow-wrap: break-word;
height: 60px;
}
#displayName {
color: white;
font-size: 1.2em;
padding: 0;
margin: 0;
}
#handle {
color: #8054f0;
font-size: 0.8em;
padding: 0;
margin: 0;
}
#postLink {
color: #8054f0;
font-size: 0.8em;
padding: 0;
margin: 0;
}
#postContent {
display: flex;
@ -95,9 +199,14 @@
padding: 0;
padding-bottom: 5px;
}
#quotingText {
font-size: 0.7em;
margin: 0;
padding: 0;
padding-bottom: 5px;
}
#postText {
margin: 0;
margin-bottom: 5px;
padding: 0;
}
#headerText {
@ -108,20 +217,68 @@
overflow: hidden;
}
#avatar {
width: 50px;
height: 50px;
height: 100%;
margin: 0px;
margin-left: 0px;
border-right: #8054f0 1px solid;
}
#embedImages {
width: 50%;
height: 50%;
margin-top: 0px;
margin-bottom: -5px;
min-width: 500px;
max-width: 500px;
max-height: 500px;
object-fit: contain;
margin: 0;
}
#carouselContainer {
position: relative;
width: 100%;
margin-top: 10px;
display: flex;
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: #4a4a4a;
}
.indicator.active {
background-color: #8054f0;
}
#prevBtn,
#nextBtn {
background-color: rgba(31, 17, 69, 0.7);
color: white;
border: 1px solid #8054f0;
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 {
width: 50%;
height: 50%;
width: 100%;
max-width: 500px;
margin-top: 10px;
align-self: center;
}
</style>

View file

@ -32,6 +32,7 @@ class Post {
authorDid: string;
authorAvatarCid: string | null;
postCid: string;
recordName: string;
authorHandle: string;
displayName: string;
text: string;
@ -47,6 +48,7 @@ class Post {
account: AccountMetadata,
) {
this.postCid = record.cid;
this.recordName = record.uri.split("/").slice(-1)[0];
this.authorDid = account.did;
this.authorAvatarCid = account.avatarCid;
this.authorHandle = account.handle;