A frontend for your PDS
1<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
2
3<script lang="ts">
4 window.ZOHOIM=window.ZOHOIM||function(a,b){ZOHOIM[a]=b;};window.ZOHOIM.prefilledMessage="";(function(){var d=document;var s=d.createElement('script');s.type='text/javascript';s.nonce='7232560246';s.defer=true;s.src="https://im.zoho.eu/api/v1/public/channel/3cd7bfa34e04555089f0c7dbfe57ed64/widget";d.getElementsByTagName('head')[0].appendChild(s); })()
5
6 import PostComponent from "./lib/PostComponent.svelte";
7 import AccountComponent from "./lib/AccountComponent.svelte";
8 import InfiniteLoading from "svelte-infinite-loading";
9 import { getNextPosts, Post, getAllMetadataFromPds, fetchPostsForUser } from "./lib/pdsfetch";
10 import { getContributors, getHeatmapData } from "./lib/tcapifetch"
11 import { Config } from "../config";
12 import { onMount } from "svelte";
13 import type { ComAtprotoRepoListRecords } from "@atcute/client/lexicons";
14 import Heatmap from "svelte5-heatmap";
15 import ContributorsModal from "./lib/ContributorsModal.svelte";
16 import DarkModeToggle from "./lib/DarkModeToggle.svelte";
17 import { isDark } from './lib/theme';
18
19 let showModal = false;
20
21 let posts: Post[] = [];
22 let postsLoaded = false;
23
24 let heatmapData: Record<string, number> = {};
25 let year = new Date().getFullYear();
26 let accountsData: any[] = [];
27 $: latestVisibleAccount = accountsData
28 ?.slice()
29 .reverse()
30 .find(a => a?.hiddenFromHomepage !== true);
31 let accountsError: Error | null = null;
32 let accountsLoaded = false;
33
34 let contributors: any[] = [];
35
36 let hue: number = 1;
37 const cycleColors = async () => {
38 while (true) {
39 hue += 1;
40 if (hue > 360) {
41 hue = 0;
42 }
43 document.documentElement.style.setProperty("--primary-h", hue.toString());
44 await new Promise((resolve) => setTimeout(resolve, 10));
45 }
46 };
47
48 let clickCounter = 0;
49 const carameldansenfusion = async () => {
50 clickCounter++;
51 if (clickCounter >= 10) {
52 clickCounter = 0;
53 cycleColors();
54 }
55 };
56
57onMount(async () => {
58 try {
59 // Load critical data first
60 accountsData = await getAllMetadataFromPds();
61 accountsLoaded = true;
62 } catch (error: unknown) {
63 accountsError = error instanceof Error ? error : new Error(String(error));
64 }
65
66 getNextPosts()
67 .then(initialPosts => {
68 posts = [...posts, ...initialPosts];
69 postsLoaded = true;
70 })
71 .catch(err => console.error("Error fetching posts:", err));
72
73 getHeatmapData()
74 .then(data => {
75 heatmapData = data;
76 })
77 .catch(err => console.error("Error fetching heatmap data:", err));
78
79 getContributors()
80 .then(data => {
81 contributors = data;
82 })
83 .catch(err => console.error("Error fetching contributors:", err));
84});
85
86
87
88 const onInfinite = ({
89 detail: { loaded, complete },
90 }: {
91 detail: { loaded: () => void; complete: () => void };
92 }) => {
93 if (!postsLoaded) {
94 console.warn("Infinite scroll triggered before initial posts loaded.");
95 return;
96 }
97
98 getNextPosts().then((newPosts) => {
99 if (newPosts.length > 0) {
100 posts = [...posts, ...newPosts];
101 loaded();
102 } else {
103 complete();
104 }
105 });
106 };
107
108</script>
109
110<main>
111 <div id="Content">
112 {#if !accountsLoaded && !accountsError}
113 <p>Loading...</p>
114 {:else if accountsError}
115 <p>Error: {accountsError.message}</p>
116 {:else}
117 <div id="Account">
118 <div>
119
120 <img
121 src={
122 $isDark
123 ? "https://public-blob.tophhie.cloud/logos/tophhiecloud-white.png"
124 : "https://public-blob.tophhie.cloud/logos/tophhiecloud-colour.png"
125 }
126 height="50"
127 alt="Tophhie Social Logo"
128 id="Logo"
129 style="padding-top:15px;"
130 />
131 <h1 onclick={carameldansenfusion} id="Header">Tophhie Social</h1>
132 <p>Home to {accountsData.length} accounts/repos 🎉</p>
133 </div>
134 <div class="button-group">
135 <a href="https://signup.tophhie.social" class="call-to-action">Sign up now!</a>
136 <a href="https://migrate.tophhie.social" class="call-to-action">Migrate your Bluesky account!</a>
137 <a href="https://discord.gg/YD8sF8JsCJ" class="call-to-action">Join the Tophhie Cloud Discord server!</a>
138 <a href="https://status.tophhie.social" class="call-to-action">Server Status</a>
139 <a href="https://ko-fi.com/tophhie" class="call-to-action">Support us and donate</a>
140 </div>
141 <div id="accountsList">
142 {#each accountsData as accountObject}
143 {#if !accountObject.hiddenFromHomepage}
144 <AccountComponent account={accountObject} />
145 {/if}
146 {/each}
147 </div>
148 <p class="disclaimer-footer">
149 {@html Config.FOOTER_TEXT}
150 <br />
151 Thank you also to our <a href="/#" onclick={() => (showModal = true)}>contributors!</a>
152 </p>
153 </div>
154 {/if}
155
156 <div id="Feed">
157 {#if heatmapData}
158 <div id="postContainer" style="padding:20px;">
159 <a style={window.innerWidth <= 768 ? "padding-bottom: 10px" : ""}>Tophhie Social Posts</a>
160 <Heatmap data={heatmapData} {year} lday={false} lmonth={window.innerWidth >= 768} />
161 </div>
162 {/if}
163 {#if accountsLoaded && latestVisibleAccount}
164 <AccountComponent account={latestVisibleAccount} welcome />
165 {/if}
166 {#each posts as postObject}
167 <PostComponent post={postObject as Post} />
168 {/each}
169 <InfiniteLoading on:infinite={onInfinite} distance={3000} />
170 </div>
171 </div>
172 <ContributorsModal bind:showModal>
173 {#snippet header()}
174 <p id="Header" style="font-size:20px; padding: 10px;">
175 Thank you to everyone who has contributed to the Tophhie Social dashboard!
176 </p>
177 {/snippet}
178
179 <ul class="contributor-list">
180 {#each contributors as contributor}
181 <li>
182 {#if contributor.avatar_url}
183 <img
184 alt="Avatar of {contributor.login}"
185 src="{contributor.avatar_url}"
186 id="avatar"
187 />
188 {/if}
189 <p>{contributor.login} • {contributor.contributions} contributions</p>
190 <a href="https://github.com/{contributor.login}"><i class="fa fa-brands fa-github" style="font-size: 20px"></i></a>
191 </li>
192 {/each}
193 </ul>
194 </ContributorsModal>
195
196 <!-- Dark Mode Toggle -->
197 <DarkModeToggle />
198</main>
199
200<style>
201</style>