bluesky client without react native baggage written in sveltekit
at main 163 lines 3.9 kB view raw
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>