your personal website on atproto - mirror blento.app

small fixes, test youtube card

Florian 209800c9 83332418

+391 -8
+1
package.json
··· 61 61 "gsap": "^3.14.2", 62 62 "link-preview-js": "^4.0.0", 63 63 "marked": "^15.0.11", 64 + "plyr": "^3.8.4", 64 65 "svelte-sonner": "^1.0.7", 65 66 "tailwind-merge": "^3.4.0", 66 67 "tailwind-variants": "^3.2.2",
+3
pnpm-lock.yaml
··· 68 68 marked: 69 69 specifier: ^15.0.11 70 70 version: 15.0.11 71 + plyr: 72 + specifier: ^3.8.4 73 + version: 3.8.4 71 74 svelte-sonner: 72 75 specifier: ^1.0.7 73 76 version: 1.0.7(svelte@5.45.8)
+35 -2
src/lib/EditableWebsite.svelte
··· 1 1 <script lang="ts"> 2 2 import { client, login } from '$lib/oauth/auth.svelte.js'; 3 3 4 - import { Navbar, Button, toast, Toaster, Toggle } from '@foxui/core'; 4 + import { Navbar, Button, toast, Toaster, Toggle, Sidebar } from '@foxui/core'; 5 5 import { BlueskyLogin } from '@foxui/social'; 6 6 7 7 import { margin, mobileMargin } from '$lib'; ··· 19 19 import { innerWidth } from 'svelte/reactivity/window'; 20 20 import { TID } from '@atproto/common-web'; 21 21 import EditingCard from './cards/Card/EditingCard.svelte'; 22 - import { CardDefinitionsByType } from './cards'; 22 + import { AllCardDefinitions, CardDefinitionsByType } from './cards'; 23 23 import { tick, type Component } from 'svelte'; 24 24 import type { CreationModalComponentProps } from './cards/types'; 25 25 import { dev } from '$app/environment'; ··· 75 75 let maxHeight = $derived(items.reduce((max, item) => Math.max(max, getY(item) + getH(item)), 0)); 76 76 77 77 function newCard(type: string = 'link') { 78 + // close sidebar if open 79 + const popover = document.getElementById('mobile-menu'); 80 + if (popover) { 81 + popover.hidePopover(); 82 + } 83 + 78 84 let item: Item = { 79 85 id: TID.nextStr(), 80 86 x: 0, ··· 167 173 description: 'Your website has been saved!' 168 174 }); 169 175 } 176 + 177 + const sidebarItems = AllCardDefinitions.filter((cardDef) => cardDef.sidebarComponent); 170 178 </script> 171 179 172 180 {#if !dev} ··· 317 325 </div> 318 326 </div> 319 327 328 + <Sidebar mobileOnly mobileClasses="lg:block"> 329 + {#each sidebarItems as cardDef} 330 + <cardDef.sidebarComponent onclick={() => newCard(cardDef.type)} /> 331 + {/each} 332 + </Sidebar> 333 + 320 334 {#if (!client.isLoggedIn && !client.isInitializing) || client.profile?.did === did} 321 335 <Navbar 322 336 class={[ ··· 389 403 /> 390 404 </svg> 391 405 </Button> 406 + 407 + {#if dev} 408 + <Button 409 + size="iconLg" 410 + variant="ghost" 411 + class="backdrop-blur-none" 412 + popovertarget="mobile-menu" 413 + > 414 + <svg 415 + xmlns="http://www.w3.org/2000/svg" 416 + fill="none" 417 + viewBox="0 0 24 24" 418 + stroke-width="1.5" 419 + stroke="currentColor" 420 + > 421 + <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> 422 + </svg> 423 + </Button> 424 + {/if} 392 425 393 426 <!-- for special stuff --> 394 427 {#if handle === 'blento.app'}
+1 -1
src/lib/cards/SpecialCards/UpdatedBlentos/MainUpdatedBlentosCards.svelte
··· 39 39 class=" hover:bg-base-200 dark:hover:bg-base-800 mb-4 flex h-fit min-w-24 flex-col items-center justify-center gap-2 rounded-xl p-2" 40 40 target="_blank" 41 41 > 42 - <div class="font-semibold">{profile.displayName || profile.handle}</div> 42 + <div class="font-semibold line-clamp-2">{profile.displayName || profile.handle}</div> 43 43 <img src={profile.avatar} class="aspect-square size-20 rounded-full" alt="" /> 44 44 </a> 45 45 {/each}
+54
src/lib/cards/YoutubeVideo/CreateYoutubeCardModal.svelte
··· 1 + <script lang="ts"> 2 + import { Alert, Button, Input, Modal, Subheading } from '@foxui/core'; 3 + import type { CreationModalComponentProps } from '../types'; 4 + import { matcher } from '.'; 5 + 6 + let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props(); 7 + 8 + let isFetchingMetadata = $state(false); 9 + 10 + let errorMessage = $state(''); 11 + 12 + const idRegExp = /^[A-Za-z0-9-_]+$/; 13 + 14 + function extractID(idOrUrl: string) { 15 + if (idRegExp.test(idOrUrl)) return idOrUrl; 16 + return matcher(idOrUrl); 17 + } 18 + 19 + async function fetchMetadata() { 20 + errorMessage = ''; 21 + 22 + const videoid = extractID(item.cardData.href); 23 + if (!videoid) { 24 + errorMessage = 'Not a valid youtube URL!'; 25 + return false; 26 + } 27 + const posterFile = 'hqdefault'; 28 + const posterURL = `https://i.ytimg.com/vi/${videoid}/${posterFile}.jpg`; 29 + 30 + item.cardData.poster = posterURL; 31 + item.cardData.youtubeId = videoid; 32 + 33 + return true; 34 + } 35 + </script> 36 + 37 + <Modal open={true} closeButton={false}> 38 + <Subheading>Enter a link to a youtube video</Subheading> 39 + <Input bind:value={item.cardData.href} /> 40 + 41 + {#if errorMessage} 42 + <Alert type="error" title="Failed to create youtube card"><span>{errorMessage}</span></Alert> 43 + {/if} 44 + 45 + <div class="mt-4 flex justify-end gap-2"> 46 + <Button onclick={oncancel} variant="ghost">Cancel</Button> 47 + <Button 48 + disabled={isFetchingMetadata} 49 + onclick={async () => { 50 + if (await fetchMetadata()) oncreate(); 51 + }}>{isFetchingMetadata ? 'Creating...' : 'Create'}</Button 52 + > 53 + </div> 54 + </Modal>
+35
src/lib/cards/YoutubeVideo/EditingYoutubeCard.svelte
··· 1 + <script lang="ts"> 2 + import type { Item } from '$lib/types'; 3 + import BaseEditingCard, { type BaseEditingCardProps } from '../BaseCard/BaseEditingCard.svelte'; 4 + import { videoPlayer } from '../utils/YoutubeVideoPlayer.svelte'; 5 + 6 + let { item = $bindable<Item>(), ...rest }: BaseEditingCardProps = $props(); 7 + </script> 8 + 9 + <BaseEditingCard {item} {...rest}> 10 + <img 11 + class={[ 12 + 'absolute inset-0 h-full w-full object-cover opacity-100 transition-transform duration-300 ease-in-out', 13 + item.cardData.href ? 'group-hover:scale-102' : '' 14 + ]} 15 + src={item.cardData.poster} 16 + alt="" 17 + /> 18 + <button 19 + onclick={() => { 20 + videoPlayer.show(item.cardData.youtubeId); 21 + }} 22 + class="absolute inset-0 flex h-full w-full cursor-pointer items-center justify-center" 23 + > 24 + <span class="sr-only"> 25 + {item.cardData.hrefText ?? 'Learn more'} 26 + </span> 27 + 28 + <svg xmlns="http://www.w3.org/2000/svg" class="text-accent-500 w-14" viewBox="0 0 256 180" 29 + ><path 30 + fill="currentColor" 31 + d="M250.346 28.075A32.18 32.18 0 0 0 227.69 5.418C207.824 0 127.87 0 127.87 0S47.912.164 28.046 5.582A32.18 32.18 0 0 0 5.39 28.24c-6.009 35.298-8.34 89.084.165 122.97a32.18 32.18 0 0 0 22.656 22.657c19.866 5.418 99.822 5.418 99.822 5.418s79.955 0 99.82-5.418a32.18 32.18 0 0 0 22.657-22.657c6.338-35.348 8.291-89.1-.164-123.134" 32 + /><path fill="#fff" d="m102.421 128.06l66.328-38.418l-66.328-38.418z" /></svg 33 + > 34 + </button> 35 + </BaseEditingCard>
+5
src/lib/cards/YoutubeVideo/SidebarItemYoutubeCard.svelte
··· 1 + <script lang="ts"> 2 + let { onclick }: { onclick: () => void } = $props(); 3 + </script> 4 + 5 + <button {onclick}> Youtube Video </button>
+39
src/lib/cards/YoutubeVideo/YoutubeCard.svelte
··· 1 + <script lang="ts"> 2 + import { marked } from 'marked'; 3 + import BaseCard, { type BaseCardProps } from '../BaseCard/BaseCard.svelte'; 4 + import { videoPlayer } from '../utils/YoutubeVideoPlayer.svelte'; 5 + 6 + let { item, ...rest }: BaseCardProps = $props(); 7 + 8 + const renderer = new marked.Renderer(); 9 + renderer.link = ({ href, title, text }) => 10 + `<a target="_blank" href="${href}" title="${title}">${text}</a>`; 11 + </script> 12 + 13 + <BaseCard {item} {...rest}> 14 + <img 15 + class={[ 16 + 'absolute inset-0 h-full w-full object-cover opacity-100 transition-transform duration-300 ease-in-out', 17 + item.cardData.href ? 'group-hover:scale-102' : '' 18 + ]} 19 + src={item.cardData.poster} 20 + alt="" 21 + /> 22 + <button 23 + onclick={() => { 24 + videoPlayer.show(item.cardData.youtubeId); 25 + }} 26 + class="absolute inset-0 flex h-full w-full cursor-pointer items-center justify-center" 27 + > 28 + <span class="sr-only"> 29 + {item.cardData.hrefText ?? 'Learn more'} 30 + </span> 31 + 32 + <svg xmlns="http://www.w3.org/2000/svg" class="text-accent-500 w-14" viewBox="0 0 256 180" 33 + ><path 34 + fill="currentColor" 35 + d="M250.346 28.075A32.18 32.18 0 0 0 227.69 5.418C207.824 0 127.87 0 127.87 0S47.912.164 28.046 5.582A32.18 32.18 0 0 0 5.39 28.24c-6.009 35.298-8.34 89.084.165 122.97a32.18 32.18 0 0 0 22.656 22.657c19.866 5.418 99.822 5.418 99.822 5.418s79.955 0 99.82-5.418a32.18 32.18 0 0 0 22.657-22.657c6.338-35.348 8.291-89.1-.164-123.134" 36 + /><path fill="#fff" d="m102.421 128.06l66.328-38.418l-66.328-38.418z" /></svg 37 + > 38 + </button></BaseCard 39 + >
+34
src/lib/cards/YoutubeVideo/index.ts
··· 1 + import type { CardDefinition } from '../types'; 2 + import CreateYoutubeCardModal from './CreateYoutubeCardModal.svelte'; 3 + import EditingYoutubeCard from './EditingYoutubeCard.svelte'; 4 + import SidebarItemYoutubeCard from './SidebarItemYoutubeCard.svelte'; 5 + import YoutubeCard from './YoutubeCard.svelte'; 6 + 7 + export const YoutubeCardDefinition = { 8 + type: 'youtubeVideo', 9 + cardComponent: YoutubeCard, 10 + editingCardComponent: EditingYoutubeCard, 11 + creationModalComponent: CreateYoutubeCardModal, 12 + createNew: (card) => { 13 + card.cardType = 'youtubeVideo'; 14 + card.cardData = {}; 15 + card.w = 2; 16 + card.mobileW = 4; 17 + }, 18 + sidebarComponent: SidebarItemYoutubeCard 19 + } as CardDefinition & { type: 'youtubeVideo' }; 20 + 21 + // Thanks to eleventy-plugin-youtube-embed 22 + // https://github.com/gfscott/eleventy-plugin-youtube-embed/blob/main/lib/extractMatches.js 23 + const urlPattern = 24 + /(?=(\s*))\1(?:<a [^>]*?>)??(?=(\s*))\2(?:https?:\/\/)??(?:w{3}\.)??(?:youtube\.com|youtu\.be)\/(?:watch\?v=|embed\/|shorts\/)??([A-Za-z0-9-_]{11})(?:[^\s<>]*)(?=(\s*))\4(?:<\/a>)??(?=(\s*))\5/; 25 + 26 + /** 27 + * Extract a YouTube ID from a URL if it matches the pattern. 28 + * @param url URL to test 29 + * @returns A YouTube video ID or undefined if none matched 30 + */ 31 + export function matcher(url: string): string | undefined { 32 + const match = url.match(urlPattern); 33 + return match?.[3]; 34 + }
+3 -1
src/lib/cards/index.ts
··· 3 3 import { UpdatedBlentosCardDefitition } from './SpecialCards/UpdatedBlentos'; 4 4 import { TextCardDefinition } from './TextCard'; 5 5 import type { CardDefinition } from './types'; 6 + import { YoutubeCardDefinition } from './YoutubeVideo'; 6 7 7 8 export const AllCardDefinitions = [ 8 9 ImageCardDefinition, 9 10 TextCardDefinition, 10 11 LinkCardDefinition, 11 - UpdatedBlentosCardDefitition 12 + UpdatedBlentosCardDefitition, 13 + YoutubeCardDefinition 12 14 ] as const; 13 15 14 16 export const CardDefinitionsByType = AllCardDefinitions.reduce(
+6
src/lib/cards/types.ts
··· 15 15 onCancel: () => void; 16 16 }; 17 17 18 + export type SidebarComponentProps = { 19 + onclick: () => void; 20 + }; 21 + 18 22 export type CardDefinition = { 19 23 cardComponent: Component<BaseCardProps>; 20 24 editingCardComponent: Component<BaseEditingCardProps>; ··· 27 31 }>; 28 32 29 33 upload?: (item: Item) => Promise<Item>; 34 + 35 + sidebarComponent?: Component<SidebarComponentProps>; 30 36 };
+155
src/lib/cards/utils/YoutubeVideoPlayer.svelte
··· 1 + <script lang="ts" module> 2 + export const videoPlayer: { 3 + id: string | undefined; 4 + 5 + show: (id: string) => void; 6 + hide: () => void; 7 + } = $state({ 8 + id: undefined, 9 + 10 + show: (id: string) => { 11 + videoPlayer.id = id; 12 + }, 13 + 14 + hide: () => { 15 + videoPlayer.id = undefined; 16 + } 17 + }); 18 + </script> 19 + 20 + <script lang="ts"> 21 + import { cn } from '@foxui/core'; 22 + import { onMount } from 'svelte'; 23 + 24 + const { class: className }: { class?: string } = $props(); 25 + 26 + let Plyr = $state(); 27 + 28 + onMount(async () => { 29 + if (!Plyr) Plyr = (await import('plyr')).default; 30 + 31 + const player = new Plyr('.js-player', { 32 + settings: ['captions', 'quality', 'loop', 'speed'], 33 + controls: [ 34 + 'play-large', 35 + 'play', 36 + 'progress', 37 + 'current-time', 38 + 'volume', 39 + 'settings', 40 + 'download', 41 + 'fullscreen' 42 + ] 43 + }); 44 + 45 + // set the video player to the id 46 + if (videoPlayer.id) { 47 + player.source = { 48 + type: 'video', 49 + sources: [ 50 + { 51 + src: videoPlayer.id, 52 + type: 'video/youtube' 53 + } 54 + ] 55 + }; 56 + } 57 + 58 + // when loaded play the video and go fullscreen 59 + player.on('ready', () => { 60 + player.play(); 61 + //player.fullscreen.enter(); 62 + }); 63 + 64 + return () => { 65 + player.destroy(); 66 + }; 67 + }); 68 + 69 + let glow = 50; 70 + </script> 71 + 72 + <svelte:head> 73 + {#if videoPlayer.id} 74 + <link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" /> 75 + {/if} 76 + </svelte:head> 77 + 78 + <svelte:window 79 + onkeydown={(e) => { 80 + if (e.key === 'Escape') { 81 + videoPlayer.hide(); 82 + } 83 + }} 84 + /> 85 + 86 + {#key videoPlayer.id} 87 + {#if videoPlayer.id} 88 + <div class="fixed inset-0 z-[100] flex h-screen w-screen items-center justify-center"> 89 + <button 90 + onclick={() => videoPlayer.hide()} 91 + class="absolute inset-0 bg-black/70 backdrop-blur-sm" 92 + > 93 + <span class="sr-only">Close</span> 94 + </button> 95 + 96 + <div 97 + class={cn( 98 + 'relative mx-4 aspect-video max-h-screen w-full overflow-hidden rounded-xl border border-black bg-white object-cover sm:mx-20 dark:border-white/10 dark:bg-white/5', 99 + className 100 + )} 101 + style="filter: url(#blur); width: 100%;" 102 + > 103 + <div class=""> 104 + <div 105 + id="player" 106 + class="h-full w-full overflow-hidden rounded-xl object-cover font-semibold text-black dark:text-white" 107 + > 108 + <div 109 + class="js-player plyr__video-embed" 110 + id="player" 111 + data-plyr-provider="youtube" 112 + data-plyr-embed-id={videoPlayer.id} 113 + ></div> 114 + </div> 115 + </div> 116 + </div> 117 + 118 + <button 119 + onclick={() => { 120 + videoPlayer.hide(); 121 + }} 122 + class="absolute top-2 right-2 z-20 rounded-full border border-white/10 bg-white/5 p-2 backdrop-blur-sm" 123 + > 124 + <svg 125 + xmlns="http://www.w3.org/2000/svg" 126 + viewBox="0 0 24 24" 127 + fill="currentColor" 128 + class="size-6" 129 + > 130 + <path 131 + fill-rule="evenodd" 132 + d="M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z" 133 + clip-rule="evenodd" 134 + /> 135 + </svg> 136 + 137 + <span class="sr-only">Close</span> 138 + </button> 139 + </div> 140 + {/if} 141 + {/key} 142 + 143 + <svg width="0" height="0"> 144 + <filter id="blur" y="-50%" x="-50%" width="300%" height="300%"> 145 + <feGaussianBlur in="SourceGraphic" stdDeviation={50} result="blurred" /> 146 + <feColorMatrix type="saturate" in="blurred" values="3" /> 147 + <feComposite in="SourceGraphic" operator="over" /> 148 + </filter> 149 + </svg> 150 + 151 + <style> 152 + * { 153 + --plyr-color-main: var(--color-accent-500); 154 + } 155 + </style>
+6 -2
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 2 import '../app.css'; 3 - 3 + 4 4 import { ThemeToggle } from '@foxui/core'; 5 5 import { onMount } from 'svelte'; 6 6 import { initClient } from '$lib/oauth'; 7 + import YoutubeVideoPlayer, { videoPlayer } from '$lib/cards/utils/YoutubeVideoPlayer.svelte'; 7 8 8 9 let { children } = $props(); 9 10 ··· 11 12 initClient(); 12 13 }); 13 14 </script> 14 - 15 15 16 16 {@render children()} 17 17 18 18 <ThemeToggle class="fixed top-2 left-2 z-10" /> 19 + 20 + {#if videoPlayer.id} 21 + <YoutubeVideoPlayer /> 22 + {/if}
+14 -2
todo.md
··· 3 3 - video card 4 4 - edit already created cards (e.g. change link) 5 5 - link card: save favicon and og image to pds 6 + - more cards list 7 + - paste handler for card creation 8 + - text cards: align text top middle bottom and left center right 6 9 - more cards: 7 10 - instagram 8 11 - github ··· 10 13 - bluesky post (fixed or latest) 11 14 - social accounts card (multiple) 12 15 - cartoons: aka https://www.opendoodles.com/ 16 + - excalidraw 13 17 - map 14 18 - youtube video 15 19 - youtube channel 16 20 - guestbook 21 + 17 22 - other atproto apps 18 23 - leaflet 19 24 - skywatched 25 + - teal.fm 26 + - tangled.sh 27 + - popfeed.social 28 + - smoke signal 29 + - statusphere.xyz 30 + - add some caching to user data 20 31 21 32 - image cards: different images for dark and light mode 22 - 23 33 24 34 - allow setting custom base and accent color 25 - - allow changing avatar and description to be different than bluesky 35 + - allow changing avatar and description to be different than bluesky 36 + - allow adding background image 37 + - borderless cards