bluesky client without react native baggage written in sveltekit
1<script lang="ts">
2 import './layout.css';
3 import Avatar from '$lib/components/Avatar.svelte';
4 import favicon from '$lib/assets/favicon.svg';
5 import { setUserContext, getUserContext } from '$lib/context';
6 import type { AppBskyActorDefs } from '@atcute/bluesky';
7 import * as TID from '@atcute/tid';
8 import { getClient, login } from '$lib/atproto';
9
10 let { children, data } = $props();
11
12 setUserContext({
13 loggedIn: data.loggedIn,
14 profile: data.profile
15 });
16
17 const user = getUserContext();
18 let handle = $state('');
19 let loggingIn = $state(false);
20 let composerDialog;
21 let postContent = $state('');
22
23 async function handleLogin() {
24 if (!handle.trim()) return;
25 loggingIn = true;
26 try {
27 await login(handle.trim());
28 } catch (exception) {
29 console.error(exception);
30 loggingIn = false;
31 }
32 }
33
34 async function createPost() {
35 if (!user.profile?.did) {
36 throw new Error('need to be authenticated to make a post');
37 }
38
39 const client = await getClient();
40 const { data, ok } = await client.post('com.atproto.repo.createRecord', {
41 input: {
42 repo: user.profile.did,
43 collection: 'app.bsky.feed.post',
44 rkey: TID.now(), // generates a sortable timestamp-based key
45 record: {
46 $type: 'app.bsky.feed.post',
47 text: postContent,
48 createdAt: new Date().toISOString(),
49 langs: ['en']
50 }
51 }
52 });
53 if (!ok) {
54 throw new Error('failed to create post');
55 }
56 console.log('success!');
57 composerDialog.close();
58 }
59</script>
60
61<svelte:head><link rel="icon" href={favicon} /></svelte:head>
62<div class="layout">
63 <div class="sidebar sidebar--left p-4">
64 {#if user.loggedIn && user.profile}
65 <div class="flex items-center gap-2">
66 <Avatar user={user.profile} />
67 {user.profile?.handle}
68 </div>
69 <button
70 class="align-items flex gap-3 rounded-full bg-post-button px-6 py-3 text-white hover:cursor-pointer"
71 onclick={() => composerDialog.showModal()}
72 >
73 New Post
74 </button>
75 {:else}
76 <form
77 onsubmit={(e) => {
78 e.preventDefault();
79 handleLogin();
80 }}
81 >
82 <input
83 type="text"
84 placeholder="handle (e.g. alice.bsky.social)"
85 bind:value={handle}
86 class="mb-2 w-full rounded border px-2 py-1 text-sm"
87 />
88 <button
89 type="submit"
90 disabled={loggingIn}
91 class="w-full rounded bg-blue-500 px-3 py-1.5 text-sm text-white hover:bg-blue-600 disabled:opacity-50"
92 >
93 {loggingIn ? 'Logging in...' : 'Log in'}
94 </button>
95 </form>
96 {/if}
97 </div>
98 <div class="main">
99 {@render children()}
100 </div>
101 <div class="sidebar sidebar--right">owo</div>
102</div>
103<dialog
104 bind:this={composerDialog}
105 class="mx-auto mt-[50px] w-full max-w-[600px] rounded-[8px] border border-modal-border p-2"
106>
107 <form
108 onsubmit={async (e) => {
109 e.preventDefault();
110 await createPost();
111 }}
112 >
113 <header class="flex h-[54px] items-center justify-between">
114 <div>
115 <button
116 type="button"
117 onclick={() => composerDialog.close()}
118 class="p-2 text-secondary-blue hover:cursor-pointer"
119 >
120 Cancel
121 </button>
122 </div>
123 <div>
124 <button class="rounded-full bg-post-button px-3.5 py-2 text-white"> Post </button>
125 </div>
126 </header>
127 <main class="flex pl-2">
128 {#if user.loggedIn && user.profile}
129 <div class="shrink-0">
130 <Avatar user={user.profile} />
131 </div>
132 {/if}
133 <textarea
134 class="m-[1px] mb-[11px] ml-[9px] min-h-[140px] grow resize-none p-[4px] text-[16.9px] leading-[24px]"
135 placeholder="What's up?"
136 bind:value={postContent}
137 ></textarea>
138 </main>
139 </form>
140</dialog>
141
142<style>
143 .sidebar {
144 position: sticky;
145 top: 0;
146 bottom: 0;
147 max-width: 279px;
148 width: 100%;
149 }
150 .layout {
151 height: 100%;
152 overflow-y: scroll;
153 display: flex;
154 justify-content: center;
155 align-items: flex-start;
156 }
157 .main {
158 width: 602px;
159 }
160 dialog::backdrop {
161 background-color: rgba(0, 0, 0, 0.8);
162 }
163</style>