[READ-ONLY] Mirror of https://github.com/flo-bit/bluesky-website-generator.
bluesky-website-generator.vercel.app
3.7 kB
148 lines
1<script lang="ts">
2 import {
3 Badge,
4 Button,
5 Heading,
6 Input,
7 Modal,
8 Subheading,
9 Tabs,
10 ThemeToggle,
11 toast,
12 Toaster
13 } from '@fuxui/base';
14 import { BlueskyLogin, blueskyPostToPostData, UserProfile, type PostData } from '@fuxui/social';
15 import { editingState } from './state.svelte.js';
16 import Editor from '$lib/Editor.svelte';
17 import Posts from '$lib/Posts.svelte';
18 import Links from '$lib/Links.svelte';
19 import { onMount } from 'svelte';
20 import { AtpBaseClient } from '@atproto/api';
21 import type { FeedViewPost } from '@atproto/api/dist/client/types/app/bsky/feed/defs.js';
22 import Navbar from '$lib/Navbar.svelte';
23
24 let { data } = $props();
25 let form = $state<HTMLFormElement | null>(null);
26
27 onMount(async () => {
28 editingState.links = JSON.parse(localStorage.getItem('links') ?? '[]');
29
30 if (!data.user) return;
31
32 const posts = await getPostsOfUser({ actor: data.user.did });
33 blueskyPosts = posts.feed.map((post) => {
34 return blueskyPostToPostData(post.post);
35 });
36 });
37
38 export async function getPostsOfUser({
39 actor,
40 cursor,
41 limit = 20
42 }: {
43 actor: string;
44 cursor?: string;
45 limit?: number;
46 }) {
47 let agent = new AtpBaseClient({ service: 'https://api.bsky.app' });
48 const perLimit = limit > 100 ? 100 : limit;
49 const posts: FeedViewPost[] = [];
50 do {
51 const { data } = await agent.app.bsky.feed.getAuthorFeed({
52 actor,
53 cursor,
54 limit: perLimit,
55 filter: 'posts_no_replies'
56 });
57 posts.push(...data.feed);
58 cursor = data.cursor;
59 } while (cursor && posts.length < limit);
60 return { feed: posts, cursor };
61 }
62
63 let blueskyPosts: PostData[] = $state([]);
64</script>
65
66<div class="mx-auto max-w-2xl py-16">
67 {#if !data.user}
68 <Heading class="mt-16 mb-8">Login with Bluesky to create your website</Heading>
69 <BlueskyLogin
70 login={async (handle) => {
71 // create and submit form with handle
72
73 if (!form) {
74 console.error('Form not found');
75 return false;
76 }
77
78 // set handle
79 const input = form.querySelector('input[name="handle"]') as HTMLInputElement;
80 if (!input) {
81 return false;
82 }
83 input.value = handle;
84 form.submit();
85
86 return true;
87 }}
88 />
89
90 <form method="POST" action="/?/login" class="hidden" bind:this={form}>
91 <input type="hidden" name="handle" value="handle" />
92 </form>
93 {:else}
94 <UserProfile class="" profile={{ ...data.user, description: undefined }} />
95
96 <div class="-mt-8">
97 <Tabs
98 active={editingState.active}
99 items={[
100 {
101 name: 'about',
102 onclick: () => (editingState.active = 'about')
103 },
104 {
105 name: 'links',
106 onclick: () => (editingState.active = 'links')
107 },
108 {
109 name: 'feed',
110 onclick: () => (editingState.active = 'feed')
111 }
112 ]}
113 />
114
115 <div class="px-4">
116 {#if editingState.active === 'about'}
117 <div
118 class="focus-within:outline-accent-400 hover:not-focus-within:bg-base-200/40 dark:hover:not-focus-within:bg-base-900/20 group focus-within:bg-accent-500/5 relative mt-4 rounded-2xl px-2 py-0.5 focus-within:outline"
119 >
120 <Editor />
121 <Badge
122 variant="secondary"
123 class="absolute top-2 right-2 opacity-60 group-focus-within:hidden"
124 >
125 Select to edit
126 </Badge>
127 </div>
128 {:else if editingState.active === 'links'}
129 <div>
130 <Links links={editingState.links} />
131 </div>
132 {:else if editingState.active === 'feed'}
133 <Posts posts={blueskyPosts} />
134 {/if}
135 </div>
136
137 <form method="POST" action="/?/logout" class="fixed right-2 bottom-2">
138 <Button variant="secondary" type="submit">Logout</Button>
139 </form>
140 </div>
141 {/if}
142</div>
143
144<Toaster />
145
146{#if data.user}
147 <Navbar handle={data.user.handle} domain={data.project} />
148{/if}