[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/
0

Configure Feed

Select the types of activity you want to include in your feed.

room / src / routes / +page.svelte
22 kB 745 lines
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 }}>&rarr;</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 }}>&larr;</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>