[READ-ONLY] Mirror of https://github.com/flo-bit/room. tiny 3d rooms saved locally or in your bluesky account, svelte/threlte
flo-bit.dev/room/
1<script lang="ts">
2 import { Canvas } from '@threlte/core';
3 import Scene from '$lib/Scene.svelte';
4 import { onMount } from 'svelte';
5 import { client, getRoom, resolveHandle } from '$lib/oauth/auth.svelte';
6 import {
7 AllObjects,
8 editorState,
9 roomState,
10 applyTransformOfSelected,
11 rotateObject,
12 type RoomObjectData,
13 saveRoomToLocalStorage,
14 currentVersion,
15 tryLoadingRoomFromLocalStorage
16 } from '$lib/state.svelte';
17 import { Button } from '$lib/components/base/button';
18 import { blueskyLoginModalState } from '$lib/components/base/modal/BlueskyLoginModal.svelte';
19 import NumberInput from '$lib/components/base/number-input/NumberInput.svelte';
20 import Modal from '$lib/components/base/modal/Modal.svelte';
21 import Heading from '$lib/components/base/heading/Heading.svelte';
22 import Subheading from '$lib/components/base/heading/Subheading.svelte';
23
24 import * as Popover from '$lib/components/base/popover';
25 import { cn } from '$lib/utils';
26 import ColorPicker from '$lib/components/extra/color-picker';
27 import {
28 hex_to_rgb,
29 okhsv_to_rgb,
30 rgb_to_hex,
31 rgb_to_okhsv
32 } from '$lib/components/extra/color-picker/color';
33 import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs';
34 import Avatar from '$lib/components/base/avatar/Avatar.svelte';
35
36 async function saveRoomToBluesky() {
37 if (!client.rpc || !client.profile) return;
38
39 applyTransformOfSelected();
40
41 editorState.selectedObject = null;
42
43 try {
44 const hello = await client.rpc.call('com.atproto.repo.putRecord', {
45 data: {
46 collection: 'dev.flo-bit.room',
47 repo: client.profile.did,
48 rkey: 'self',
49 record: {
50 roomState: JSON.parse(
51 JSON.stringify({
52 wallColor: roomState.wallColor,
53 floorColor: roomState.floorColor,
54 objects: roomState.objects,
55 size: roomState.size,
56 id: roomState.id,
57 version: roomState.version
58 })
59 )
60 }
61 }
62 });
63
64 // @ts-ignore
65 if (window.umami) {
66 // @ts-ignore
67 window.umami?.track('room_save', {
68 handle: client.profile.handle,
69 objectCount: roomState.objects.length
70 });
71 }
72
73 // TODO: show success modal
74 successModalState = true;
75 } catch (e) {
76 console.error(e);
77 }
78 }
79
80 let profile: ProfileViewDetailed | null = $state(null);
81
82 let loading = $state(true);
83
84 let currentHandle = $state('flo-bit.dev');
85
86 let successModalState = $state(false);
87
88 let signInSuccessModalState = $state(false);
89
90 async function loadRoomFromBluesky(handle: string) {
91 const did = await resolveHandle({ handle });
92 console.log(did);
93 const room = await getRoom({ did });
94 console.log(room);
95
96 roomState.wallColor = room.room.wallColor;
97 roomState.floorColor = room.room.floorColor;
98
99 let availableObjects = new Set(Object.keys(AllObjects));
100
101 roomState.objects = room.room.objects.filter((object: RoomObjectData) =>
102 availableObjects.has(object.kind)
103 );
104 if (roomState.objects.length !== room.room.objects.length) {
105 console.warn('some objects were not found and removed!');
106 }
107
108 roomState.size = room.room.size;
109 roomState.id = room.room.id;
110 roomState.version = room.room.version;
111
112 profile = room.profile;
113
114 // @ts-ignore
115 if (window.umami) {
116 // @ts-ignore
117 window.umami.track('room_view', {
118 handle: handle
119 });
120 }
121 }
122
123 onMount(async () => {
124 if (client.justLoggedIn) {
125 successModalState = true;
126 client.justLoggedIn = false;
127 }
128
129 window.addEventListener('keydown', (e) => {
130 if (e.key === 'x') {
131 deleteSelectedObject();
132 }
133 });
134
135 // get handle from url search params
136 let handle = new URLSearchParams(window.location.search).get('handle');
137 if (!handle) {
138 handle = 'flo-bit.dev';
139 }
140
141 currentHandle = handle;
142
143 await loadRoomFromBluesky(handle);
144
145 loading = false;
146 });
147
148 let roomSettingsModalState = $state(false);
149 let createRoomModalState = $state(false);
150 let infoModalState = $state(false);
151
152 let okhsv = $state({ h: 180, s: 0.0, v: 0.95 });
153 let okhsv2 = $state({ h: 180, s: 0.0, v: 0.95 });
154 let okhsv3 = $state({ h: 180, s: 0.0, v: 0.95 });
155 let okhsv4 = $state({ h: 180, s: 0.0, v: 0.95 });
156 let okhsv5 = $state({ h: 180, s: 0.0, v: 0.95 });
157
158 let okhsv_wall = $state({ h: 180, s: 0.0, v: 0.95 });
159 let okhsv_floor = $state({ h: 180, s: 0.0, v: 0.95 });
160
161 function deleteSelectedObject() {
162 roomState.objects = roomState.objects.filter((object) => object !== editorState.selectedObject);
163
164 editorState.selectedObject = null;
165 }
166
167 $effect(() => {
168 if (!editorState.selectedObject) return;
169
170 okhsv = rgb_to_okhsv(hex_to_rgb(editorState.selectedObject.colors[0]));
171 okhsv2 = rgb_to_okhsv(hex_to_rgb(editorState.selectedObject.colors[1]));
172 okhsv3 = rgb_to_okhsv(hex_to_rgb(editorState.selectedObject.colors[2]));
173
174 okhsv_wall = rgb_to_okhsv(hex_to_rgb(roomState.wallColor));
175 okhsv_floor = rgb_to_okhsv(hex_to_rgb(roomState.floorColor));
176 });
177
178 $effect(() => {
179 if (client.justLoggedIn) {
180 signInSuccessModalState = true;
181 client.justLoggedIn = false;
182 }
183 });
184</script>
185
186<div class="h-[100dvh] w-screen">
187 <Canvas>
188 <Scene />
189 </Canvas>
190</div>
191
192{#if editorState.isEditing}
193 <Button
194 class="fixed right-4 bottom-4"
195 onclick={() => {
196 editorState.startIndex += editorState.shownCount;
197 if (editorState.startIndex >= Object.keys(AllObjects).length) {
198 editorState.startIndex = 0;
199 }
200 }}>→</Button
201 >
202
203 <Button
204 class="fixed bottom-4 left-4"
205 onclick={() => {
206 editorState.startIndex -= editorState.shownCount;
207 if (editorState.startIndex < 0) {
208 editorState.startIndex = Object.keys(AllObjects).length - editorState.shownCount;
209 }
210 }}>←</Button
211 >
212
213 <div class="fixed top-4 left-4 flex flex-col items-start gap-2">
214 <Button
215 size="iconLg"
216 onclick={() => {
217 roomSettingsModalState = true;
218 }}
219 >
220 <svg
221 xmlns="http://www.w3.org/2000/svg"
222 viewBox="0 0 24 24"
223 fill="currentColor"
224 class="size-6"
225 >
226 <path
227 fill-rule="evenodd"
228 d="M11.078 2.25c-.917 0-1.699.663-1.85 1.567L9.05 4.889c-.02.12-.115.26-.297.348a7.493 7.493 0 0 0-.986.57c-.166.115-.334.126-.45.083L6.3 5.508a1.875 1.875 0 0 0-2.282.819l-.922 1.597a1.875 1.875 0 0 0 .432 2.385l.84.692c.095.078.17.229.154.43a7.598 7.598 0 0 0 0 1.139c.015.2-.059.352-.153.43l-.841.692a1.875 1.875 0 0 0-.432 2.385l.922 1.597a1.875 1.875 0 0 0 2.282.818l1.019-.382c.115-.043.283-.031.45.082.312.214.641.405.985.57.182.088.277.228.297.35l.178 1.071c.151.904.933 1.567 1.85 1.567h1.844c.916 0 1.699-.663 1.85-1.567l.178-1.072c.02-.12.114-.26.297-.349.344-.165.673-.356.985-.57.167-.114.335-.125.45-.082l1.02.382a1.875 1.875 0 0 0 2.28-.819l.923-1.597a1.875 1.875 0 0 0-.432-2.385l-.84-.692c-.095-.078-.17-.229-.154-.43a7.614 7.614 0 0 0 0-1.139c-.016-.2.059-.352.153-.43l.84-.692c.708-.582.891-1.59.433-2.385l-.922-1.597a1.875 1.875 0 0 0-2.282-.818l-1.02.382c-.114.043-.282.031-.449-.083a7.49 7.49 0 0 0-.985-.57c-.183-.087-.277-.227-.297-.348l-.179-1.072a1.875 1.875 0 0 0-1.85-1.567h-1.843ZM12 15.75a3.75 3.75 0 1 0 0-7.5 3.75 3.75 0 0 0 0 7.5Z"
229 clip-rule="evenodd"
230 />
231 </svg>
232 </Button>
233
234 {#if editorState.selectedObject}
235 <Button
236 variant="red"
237 size="icon"
238 class="mt-8"
239 onclick={() => {
240 roomState.objects = roomState.objects.filter(
241 (object) => object !== editorState.selectedObject
242 );
243
244 editorState.selectedObject = null;
245 }}
246 >
247 <svg
248 xmlns="http://www.w3.org/2000/svg"
249 viewBox="0 0 24 24"
250 fill="currentColor"
251 class="size-6"
252 >
253 <path
254 fill-rule="evenodd"
255 d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z"
256 clip-rule="evenodd"
257 />
258 </svg>
259
260 <span class="sr-only">Delete selected object</span>
261 </Button>
262
263 <div class="mt-4">
264 <Button
265 size="icon"
266 onclick={() => {
267 rotateObject(-Math.PI / 6);
268 }}
269 variant="secondary"
270 >
271 <svg
272 xmlns="http://www.w3.org/2000/svg"
273 fill="none"
274 viewBox="0 0 24 24"
275 stroke-width="1.5"
276 stroke="currentColor"
277 class="-scale-x-100"
278 >
279 <path
280 stroke-linecap="round"
281 stroke-linejoin="round"
282 d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
283 />
284 </svg>
285 </Button>
286
287 <Button
288 size="icon"
289 onclick={() => {
290 rotateObject(Math.PI / 6);
291 }}
292 variant="secondary"
293 >
294 <svg
295 xmlns="http://www.w3.org/2000/svg"
296 fill="none"
297 viewBox="0 0 24 24"
298 stroke-width="1.5"
299 stroke="currentColor"
300 class=""
301 >
302 <path
303 stroke-linecap="round"
304 stroke-linejoin="round"
305 d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
306 />
307 </svg>
308 </Button>
309 </div>
310
311 <Popover.Root>
312 <Popover.Trigger class={cn('mt-5 ml-1 cursor-pointer')}>
313 <div
314 class="border-base-300 dark:border-base-700 z-10 size-8 rounded-full border"
315 style={`background-color: ${editorState.selectedObject.colors[0]};`}
316 ></div>
317 </Popover.Trigger>
318 <Popover.Content side="right" sideOffset={10}>
319 <ColorPicker
320 bind:okhsv
321 on:change={(e) => {
322 if (!editorState.selectedObject) return;
323
324 const rgb = okhsv_to_rgb(okhsv);
325 editorState.selectedObject.colors[0] = rgb_to_hex(rgb);
326 }}
327 />
328 </Popover.Content>
329 </Popover.Root>
330
331 {#if AllObjects[editorState.selectedObject.kind].colors > 1}
332 <Popover.Root>
333 <Popover.Trigger class={cn('mt-2 ml-1 cursor-pointer')}>
334 <div
335 class="border-base-300 dark:border-base-700 z-10 size-8 rounded-full border"
336 style={`background-color: ${editorState.selectedObject.colors[1]};`}
337 ></div>
338 </Popover.Trigger>
339 <Popover.Content side="left" sideOffset={10}>
340 <ColorPicker
341 bind:okhsv={okhsv2}
342 on:change={(e) => {
343 if (!editorState.selectedObject) return;
344
345 const rgb = okhsv_to_rgb(okhsv2);
346 editorState.selectedObject.colors[1] = rgb_to_hex(rgb);
347
348 saveRoomToLocalStorage();
349 }}
350 />
351 </Popover.Content>
352 </Popover.Root>
353 {/if}
354
355 {#if AllObjects[editorState.selectedObject.kind].colors > 2}
356 <Popover.Root>
357 <Popover.Trigger class={cn('mt-2 ml-1 cursor-pointer')}>
358 <div
359 class="border-base-300 dark:border-base-700 z-10 size-8 rounded-full border"
360 style={`background-color: ${editorState.selectedObject.colors[2]};`}
361 ></div>
362 </Popover.Trigger>
363 <Popover.Content side="left" sideOffset={10}>
364 <ColorPicker
365 bind:okhsv={okhsv3}
366 on:change={(e) => {
367 if (!editorState.selectedObject) return;
368
369 const rgb = okhsv_to_rgb(okhsv3);
370 editorState.selectedObject.colors[2] = rgb_to_hex(rgb);
371
372 saveRoomToLocalStorage();
373 }}
374 />
375 </Popover.Content>
376 </Popover.Root>
377 {/if}
378
379 {#if AllObjects[editorState.selectedObject.kind].colors > 3}
380 <Popover.Root>
381 <Popover.Trigger class={cn('mt-2 ml-1 cursor-pointer')}>
382 <div
383 class="border-base-300 dark:border-base-700 z-10 size-8 rounded-full border"
384 style={`background-color: ${editorState.selectedObject.colors[3]};`}
385 ></div>
386 </Popover.Trigger>
387 <Popover.Content side="left" sideOffset={10}>
388 <ColorPicker
389 bind:okhsv={okhsv4}
390 on:change={(e) => {
391 if (!editorState.selectedObject) return;
392
393 const rgb = okhsv_to_rgb(okhsv4);
394 editorState.selectedObject.colors[3] = rgb_to_hex(rgb);
395
396 saveRoomToLocalStorage();
397 }}
398 />
399 </Popover.Content>
400 </Popover.Root>
401 {/if}
402
403 {#if AllObjects[editorState.selectedObject.kind].colors > 4}
404 <Popover.Root>
405 <Popover.Trigger class={cn('mt-2 ml-1 cursor-pointer')}>
406 <div
407 class="border-base-300 dark:border-base-700 z-10 size-8 rounded-full border"
408 style={`background-color: ${editorState.selectedObject.colors[4]};`}
409 ></div>
410 </Popover.Trigger>
411 <Popover.Content side="left" sideOffset={10}>
412 <ColorPicker
413 bind:okhsv={okhsv5}
414 on:change={(e) => {
415 if (!editorState.selectedObject) return;
416
417 const rgb = okhsv_to_rgb(okhsv5);
418 editorState.selectedObject.colors[4] = rgb_to_hex(rgb);
419
420 saveRoomToLocalStorage();
421 }}
422 />
423 </Popover.Content>
424 </Popover.Root>
425 {/if}
426 {/if}
427 </div>
428 <div class="fixed top-4 right-4 flex flex-col items-end gap-2">
429 {#if !client || !client.isLoggedIn}
430 <Button
431 onclick={() => {
432 applyTransformOfSelected();
433
434 blueskyLoginModalState.open = true;
435 }}
436 >
437 <svg
438 role="img"
439 viewBox="0 0 24 24"
440 xmlns="http://www.w3.org/2000/svg"
441 aria-hidden="true"
442 fill="currentColor"
443 ><path
444 d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z"
445 /></svg
446 >
447 Login</Button
448 >
449 {:else}
450 <Button
451 onclick={() => {
452 saveRoomToBluesky();
453 }}>Save & Share</Button
454 >
455 {/if}
456
457 <Popover.Root>
458 <Popover.Trigger class={cn('mt-5 ml-1 cursor-pointer')}>
459 <div
460 class="border-base-300 dark:border-base-700 z-10 size-8 rounded-full border"
461 style={`background-color: ${roomState.wallColor};`}
462 ></div>
463 </Popover.Trigger>
464 <Popover.Content side="right" sideOffset={10}>
465 <ColorPicker
466 bind:okhsv={okhsv_wall}
467 on:change={(e) => {
468 const rgb = okhsv_to_rgb(okhsv_wall);
469 roomState.wallColor = rgb_to_hex(rgb);
470 saveRoomToLocalStorage();
471 }}
472 />
473 </Popover.Content>
474 </Popover.Root>
475
476 <Popover.Root>
477 <Popover.Trigger class={cn('mt-5 ml-1 cursor-pointer')}>
478 <div
479 class="border-base-300 dark:border-base-700 z-10 size-8 rounded-full border"
480 style={`background-color: ${roomState.floorColor};`}
481 ></div>
482 </Popover.Trigger>
483 <Popover.Content side="right" sideOffset={10}>
484 <ColorPicker
485 bind:okhsv={okhsv_floor}
486 on:change={(e) => {
487 const rgb = okhsv_to_rgb(okhsv_floor);
488 roomState.floorColor = rgb_to_hex(rgb);
489 saveRoomToLocalStorage();
490 }}
491 />
492 </Popover.Content>
493 </Popover.Root>
494 </div>
495
496 <Modal bind:open={roomSettingsModalState}>
497 <div class="flex flex-col items-start gap-4">
498 <Heading>Room settings</Heading>
499
500 <Button
501 variant="secondary"
502 class="mt-4"
503 onclick={() => {
504 currentHandle = client.profile?.handle ?? '';
505 profile = client.profile;
506 editorState.isEditing = false;
507 roomSettingsModalState = false;
508 }}
509 >
510 Stop editing & Preview
511 </Button>
512
513 <Subheading class="mt-2">Room Size</Subheading>
514 <div class="flex items-center gap-2">
515 <NumberInput
516 bind:value={roomState.size.x}
517 min={1}
518 max={8}
519 onchange={() => saveRoomToLocalStorage()}
520 />
521 <span class="text-accent-700 dark:text-accent-300">
522 <svg
523 xmlns="http://www.w3.org/2000/svg"
524 fill="none"
525 viewBox="0 0 24 24"
526 stroke-width="3"
527 stroke="currentColor"
528 class="size-4"
529 >
530 <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
531 </svg>
532 </span>
533 <NumberInput
534 bind:value={roomState.size.z}
535 min={1}
536 max={8}
537 onchange={() => saveRoomToLocalStorage()}
538 />
539 </div>
540
541 <Subheading class="mt-4">Danger zone</Subheading>
542 <div class="mt-4 flex gap-2">
543 {#if client.isLoggedIn}
544 <Button
545 variant="red"
546 onclick={async () => {
547 if (!client.profile?.handle) {
548 console.error('no handle found');
549 return;
550 }
551 await loadRoomFromBluesky(client.profile.handle);
552 roomSettingsModalState = false;
553 }}
554 >
555 Load from bluesky
556 </Button>
557 {/if}
558
559 <Button
560 variant="red"
561 onclick={() => {
562 roomState.objects = [];
563 editorState.selectedObject = null;
564 roomState.size = { x: 2, z: 3 };
565 roomState.wallColor = '#f1f1f1';
566 roomState.floorColor = '#a1a1a1';
567
568 saveRoomToLocalStorage();
569 }}
570 >
571 Clear room
572 </Button>
573 </div>
574 </div>
575 </Modal>
576{:else if !loading}
577 <div
578 class="pointer-events-none fixed top-8 right-4 left-4 flex items-center justify-center gap-2"
579 >
580 {#if currentHandle}
581 <Avatar src={profile?.avatar} />
582 {/if}
583 <Heading>
584 <a
585 href={`https://bsky.app/profile/${profile?.handle}`}
586 target="_blank"
587 class="hover:text-accent-600 dark:hover:text-accent-500 pointer-events-auto"
588 >{currentHandle}'s</a
589 >
590 room</Heading
591 >
592 </div>
593
594 <div class="pointer-events-none fixed right-4 bottom-4 left-4 flex justify-between">
595 <Button
596 onclick={() => {
597 infoModalState = true;
598 }}
599 variant="secondary"
600 >
601 Info
602 </Button>
603
604 <Button
605 onclick={() => {
606 if (client.profile?.handle == currentHandle) {
607 editorState.isEditing = true;
608 return;
609 }
610
611 tryLoadingRoomFromLocalStorage(true);
612
613 editorState.isEditing = true;
614 }}
615 >
616 {client.profile?.handle == currentHandle ? 'Edit room' : 'Create room'}
617 </Button>
618 </div>
619{:else}
620 <div
621 class="bg-base-100 dark:bg-base-950 fixed inset-0 flex h-[100dvh] w-screen items-center justify-center"
622 >
623 <Heading>Loading...</Heading>
624 </div>
625{/if}
626
627<Modal bind:open={infoModalState} class="text-base-900 dark:text-base-50">
628 <div>
629 This is an experimental web app for creating tiny 3d rooms (and sharing them on bluesky) built
630 by
631 <a
632 href="https://flo-bit.dev"
633 target="_blank"
634 class="text-accent-700 dark:text-accent-400 hover:text-accent-600 dark:hover:text-accent-500 font-medium"
635 >flo-bit</a
636 >
637 </div>
638 <div>
639 It's still a work in progress, but it's open source (<a
640 href="https://github.com/flo-bit/room"
641 target="_blank"
642 class="text-accent-700 dark:text-accent-400 hover:text-accent-600 dark:hover:text-accent-500 font-medium"
643 >source code available here</a
644 >), so feel free to contribute or suggest features on github 😊
645 </div>
646
647 <div>
648 You can also send me a message on <a
649 href="https://bsky.app/profile/flo-bit.dev"
650 target="_blank"
651 class="text-accent-700 dark:text-accent-400 hover:text-accent-600 dark:hover:text-accent-500 font-medium"
652 >bluesky</a
653 > (or tag me when sharing your room so I can admire your creation 😊)
654 </div>
655</Modal>
656
657<Modal bind:open={createRoomModalState}></Modal>
658
659<Modal bind:open={successModalState}>
660 <Heading class="flex items-center gap-4">
661 <svg
662 xmlns="http://www.w3.org/2000/svg"
663 fill="none"
664 viewBox="0 0 24 24"
665 stroke-width="1.5"
666 stroke="currentColor"
667 class="text-accent-600 dark:text-accent-400 size-8"
668 >
669 <path
670 stroke-linecap="round"
671 stroke-linejoin="round"
672 d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
673 />
674 </svg>
675
676 Room saved to your profile</Heading
677 >
678 <Button
679 size="lg"
680 class="mt-8 py-4"
681 href={'https://bsky.app/intent/compose?text=' +
682 encodeURIComponent(
683 `Check out my bluesky room: https://flo-bit.dev/room/?handle=${client.profile?.handle}`
684 )}
685 target="_blank"
686 >
687 <svg
688 role="img"
689 viewBox="0 0 24 24"
690 xmlns="http://www.w3.org/2000/svg"
691 aria-hidden="true"
692 fill="currentColor"
693 ><path
694 d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z"
695 /></svg
696 >
697 Share on Bluesky</Button
698 >
699</Modal>
700
701<Modal bind:open={signInSuccessModalState}>
702 <Heading class="flex items-center gap-4">
703 <svg
704 role="img"
705 viewBox="0 0 24 24"
706 xmlns="http://www.w3.org/2000/svg"
707 aria-hidden="true"
708 fill="currentColor"
709 class="text-accent-600 dark:text-accent-400 size-8"
710 ><path
711 d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z"
712 /></svg
713 >
714 Sign in successful</Heading
715 >
716
717 <div class="text-base-800 dark:text-base-200 flex flex-col gap-2 text-sm">
718 <p>If you haven't already, create a room to get started.</p>
719
720 <p>You can then save your room to your profile and share it on bluesky.</p>
721
722 <p>Feel free to tag me when sharing your room so I can admire your creation 😊</p>
723
724 <p class="mt-4">
725 my handle: <a
726 href="https://bsky.app/profile/flo-bit.dev"
727 target="_blank"
728 class="text-accent-700 dark:text-accent-400 hover:text-accent-600 dark:hover:text-accent-500 font-medium"
729 >@flo-bit.dev</a
730 >
731 </p>
732
733 <Button
734 class="mt-4"
735 onclick={() => {
736 tryLoadingRoomFromLocalStorage(true);
737
738 editorState.isEditing = true;
739 signInSuccessModalState = false;
740 }}
741 >
742 Go to my room
743 </Button>
744 </div>
745</Modal>