your personal website on atproto - mirror blento.app

add map location card

Florian 0c4fcf75 c96a01b5

+242 -17
+1
package.json
··· 61 61 "bits-ui": "^2.14.4", 62 62 "clsx": "^2.1.1", 63 63 "gsap": "^3.14.2", 64 + "leaflet": "^1.9.4", 64 65 "link-preview-js": "^4.0.0", 65 66 "marked": "^15.0.11", 66 67 "plyr": "^3.8.4",
+8
pnpm-lock.yaml
··· 68 68 gsap: 69 69 specifier: ^3.14.2 70 70 version: 3.14.2 71 + leaflet: 72 + specifier: ^1.9.4 73 + version: 1.9.4 71 74 link-preview-js: 72 75 specifier: ^4.0.0 73 76 version: 4.0.0 ··· 1887 1890 1888 1891 known-css-properties@0.35.0: 1889 1892 resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==, tarball: https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz} 1893 + 1894 + leaflet@1.9.4: 1895 + resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==, tarball: https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz} 1890 1896 1891 1897 levn@0.4.1: 1892 1898 resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, tarball: https://registry.npmjs.org/levn/-/levn-0.4.1.tgz} ··· 4436 4442 kleur@4.1.5: {} 4437 4443 4438 4444 known-css-properties@0.35.0: {} 4445 + 4446 + leaflet@1.9.4: {} 4439 4447 4440 4448 levn@0.4.1: 4441 4449 dependencies:
+12 -4
src/lib/EditableWebsite.svelte
··· 175 175 }); 176 176 } 177 177 178 - const sidebarItems = AllCardDefinitions.filter((cardDef) => cardDef.sidebarComponent); 178 + const sidebarItems = AllCardDefinitions.filter( 179 + (cardDef) => cardDef.sidebarComponent || cardDef.sidebarButtonText 180 + ); 179 181 </script> 180 182 181 183 {#if !dev} ··· 328 330 </div> 329 331 </div> 330 332 331 - <Sidebar mobileOnly mobileClasses="lg:block p-4"> 332 - <div> 333 + <Sidebar mobileOnly mobileClasses="lg:block p-4 gap-4"> 334 + <div class="flex flex-col gap-2"> 333 335 {#each sidebarItems as cardDef} 334 - <cardDef.sidebarComponent onclick={() => newCard(cardDef.type)} /> 336 + {#if cardDef.sidebarComponent} 337 + <cardDef.sidebarComponent onclick={() => newCard(cardDef.type)} /> 338 + {:else if cardDef.sidebarButtonText} 339 + <Button onclick={() => newCard(cardDef.type)} variant="ghost" class="w-full justify-start" 340 + >{cardDef.sidebarButtonText}</Button 341 + > 342 + {/if} 335 343 {/each} 336 344 </div> 337 345 </Sidebar>
+1 -1
src/lib/cards/BlueskyPostCard/SidebarItemBlueskyPostCard.svelte
··· 4 4 let { onclick }: { onclick: () => void } = $props(); 5 5 </script> 6 6 7 - <Button {onclick} variant="ghost" class="w-full" size="lg"> 7 + <Button {onclick} variant="ghost" class="w-full justify-start"> 8 8 <svg 9 9 xmlns="http://www.w3.org/2000/svg" 10 10 version="1.1"
+5 -7
src/lib/cards/EmbedCard/SidebarItemEmbedCard.svelte
··· 4 4 let { onclick }: { onclick: () => void } = $props(); 5 5 </script> 6 6 7 - <Button {onclick} variant="ghost" class="w-full" size="lg"> 7 + <Button {onclick} variant="ghost" class="w-full justify-start"> 8 8 <svg 9 9 xmlns="http://www.w3.org/2000/svg" 10 - fill="none" 11 10 viewBox="0 0 24 24" 12 - stroke-width="1.5" 13 - stroke="currentColor" 11 + fill="currentColor" 14 12 class="text-accent-600 dark:text-accent-400" 15 13 > 16 14 <path 17 - stroke-linecap="round" 18 - stroke-linejoin="round" 19 - d="M14.25 9.75 16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0 0 20.25 18V6A2.25 2.25 0 0 0 18 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25Z" 15 + fill-rule="evenodd" 16 + d="M3 6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6Zm14.25 6a.75.75 0 0 1-.22.53l-2.25 2.25a.75.75 0 1 1-1.06-1.06L15.44 12l-1.72-1.72a.75.75 0 1 1 1.06-1.06l2.25 2.25c.141.14.22.331.22.53Zm-10.28-.53a.75.75 0 0 0 0 1.06l2.25 2.25a.75.75 0 1 0 1.06-1.06L8.56 12l1.72-1.72a.75.75 0 1 0-1.06-1.06l-2.25 2.25Z" 17 + clip-rule="evenodd" 20 18 /> 21 19 </svg> 22 20
+1 -1
src/lib/cards/LivestreamCard/SidebarItemEmbedLivestreamCard.svelte
··· 5 5 let { onclick }: { onclick: () => void } = $props(); 6 6 </script> 7 7 8 - <Button {onclick} variant="ghost" class="w-full" size="lg"> 8 + <Button {onclick} variant="ghost" class="w-full justify-start"> 9 9 <Icon class="size-4" /> 10 10 11 11 Embed stream.place
+1 -1
src/lib/cards/LivestreamCard/SidebarItemLivestreamCard.svelte
··· 5 5 let { onclick }: { onclick: () => void } = $props(); 6 6 </script> 7 7 8 - <Button {onclick} variant="ghost" class="w-full" size="lg"> 8 + <Button {onclick} variant="ghost" class="w-full justify-start"> 9 9 <Icon class="size-4" /> 10 10 11 11 Latest stream.place
+56
src/lib/cards/MapCard/CreateMapCardModal.svelte
··· 1 + <script lang="ts"> 2 + import { Alert, Button, Input, Modal, Subheading } from '@foxui/core'; 3 + import type { CreationModalComponentProps } from '../types'; 4 + 5 + let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props(); 6 + 7 + let isFetchingLocation = $state(false); 8 + 9 + let errorMessage = $state(''); 10 + 11 + let search = $state(''); 12 + 13 + async function fetchLocation() { 14 + errorMessage = ''; 15 + isFetchingLocation = true; 16 + 17 + try { 18 + const response = await fetch('/api/geocoding?q=' + encodeURIComponent(search)); 19 + if (response.ok) { 20 + const data = await response.json(); 21 + 22 + if (!data.lat || !data.lon) throw new Error('lat or lon not found'); 23 + 24 + item.cardData.lat = parseFloat(data.lat); 25 + item.cardData.lon = parseFloat(data.lon); 26 + } else { 27 + throw new Error('response not ok'); 28 + } 29 + } catch (error) { 30 + errorMessage = "Couldn't find that location!"; 31 + return false; 32 + } finally { 33 + isFetchingLocation = false; 34 + } 35 + return true; 36 + } 37 + </script> 38 + 39 + <Modal open={true} closeButton={false}> 40 + <Subheading>Enter a city and country</Subheading> 41 + <Input bind:value={search} /> 42 + 43 + {#if errorMessage} 44 + <Alert type="error" title="Failed to create map card"><span>{errorMessage}</span></Alert> 45 + {/if} 46 + 47 + <div class="mt-4 flex justify-end gap-2"> 48 + <Button onclick={oncancel} variant="ghost">Cancel</Button> 49 + <Button 50 + disabled={isFetchingLocation} 51 + onclick={async () => { 52 + if (await fetchLocation()) oncreate(); 53 + }}>{isFetchingLocation ? 'Creating...' : 'Create'}</Button 54 + > 55 + </div> 56 + </Modal>
+80
src/lib/cards/MapCard/Map.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import 'leaflet/dist/leaflet.css'; 4 + import type { Item } from '$lib/types'; 5 + 6 + let { item }: { item: Item } = $props(); 7 + 8 + let lMap; 9 + let leaflet; 10 + 11 + let map: HTMLElement | undefined = $state(); 12 + 13 + function getCSSVar(variable: string) { 14 + return getComputedStyle(document.body).getPropertyValue(variable).trim(); 15 + } 16 + 17 + onMount(async () => { 18 + try { 19 + console.log(`Loading map...`); 20 + 21 + // @ts-ignore 22 + leaflet = await import('leaflet'); 23 + 24 + const location = [item.cardData.lat, item.cardData.lon]; 25 + 26 + lMap = leaflet 27 + .map(map, { 28 + zoomControl: false, 29 + dragging: false, 30 + minZoom: 2, 31 + maxZoom: 5 32 + }) 33 + .setView(location, 3.5); 34 + leaflet 35 + .tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { 36 + attribution: 37 + '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' 38 + }) 39 + .addTo(lMap); 40 + 41 + let color = 42 + !item.color || item.color === 'transparent' || item.color === 'base' 43 + ? 'accent' 44 + : item.color; 45 + 46 + console.log(color); 47 + 48 + const computedColor = getCSSVar(`--color-${color}-500`); 49 + 50 + leaflet 51 + .circle(location, { 52 + color: computedColor, 53 + fillColor: computedColor, 54 + fillOpacity: 0.5, 55 + radius: 100000, 56 + class: '!grayscale-0' 57 + }) 58 + .addTo(lMap); 59 + } catch (err) { 60 + console.error(`Something went wrong trying to get the geolocation data`, err); 61 + } 62 + }); 63 + </script> 64 + 65 + <div 66 + bind:this={map} 67 + class={[ 68 + 'absolute inset-0 isolate h-full w-full pointer-coarse:pointer-events-none', 69 + item.color && item.color !== 'base' && item.color !== 'transparent' ? 'mix-blend-multiply' : '' 70 + ]} 71 + ></div> 72 + 73 + <style> 74 + :global(:not(.dark)) :global(.leaflet-layer) { 75 + filter: grayscale(100%); 76 + } 77 + :global(.dark) :global(.leaflet-layer) { 78 + filter: grayscale(100%) invert(100%); 79 + } 80 + </style>
+10
src/lib/cards/MapCard/MapCard.svelte
··· 1 + <script lang="ts"> 2 + import type { Item } from '$lib/types'; 3 + import Map from './Map.svelte'; 4 + 5 + let { item }: { item: Item } = $props(); 6 + </script> 7 + 8 + {#key item.color} 9 + <Map {item} /> 10 + {/key}
+22
src/lib/cards/MapCard/SidebarItemMapCard.svelte
··· 1 + <script lang="ts"> 2 + import { Button } from '@foxui/core'; 3 + 4 + let { onclick }: { onclick: () => void } = $props(); 5 + </script> 6 + 7 + <Button {onclick} variant="ghost" class="w-full justify-start"> 8 + <svg 9 + xmlns="http://www.w3.org/2000/svg" 10 + viewBox="0 0 24 24" 11 + fill="currentColor" 12 + class="text-accent-600 dark:text-accent-400" 13 + > 14 + <path 15 + fill-rule="evenodd" 16 + d="M8.161 2.58a1.875 1.875 0 0 1 1.678 0l4.993 2.498c.106.052.23.052.336 0l3.869-1.935A1.875 1.875 0 0 1 21.75 4.82v12.485c0 .71-.401 1.36-1.037 1.677l-4.875 2.437a1.875 1.875 0 0 1-1.676 0l-4.994-2.497a.375.375 0 0 0-.336 0l-3.868 1.935A1.875 1.875 0 0 1 2.25 19.18V6.695c0-.71.401-1.36 1.036-1.677l4.875-2.437ZM9 6a.75.75 0 0 1 .75.75V15a.75.75 0 0 1-1.5 0V6.75A.75.75 0 0 1 9 6Zm6.75 3a.75.75 0 0 0-1.5 0v8.25a.75.75 0 0 0 1.5 0V9Z" 17 + clip-rule="evenodd" 18 + /> 19 + </svg> 20 + 21 + Map with Location 22 + </Button>
+19
src/lib/cards/MapCard/index.ts
··· 1 + import type { CardDefinition } from '../types'; 2 + import CreateMapCardModal from './CreateMapCardModal.svelte'; 3 + import MapCard from './MapCard.svelte'; 4 + import SidebarItemMapCard from './SidebarItemMapCard.svelte'; 5 + 6 + export const MapCardDefinition = { 7 + type: 'mapLocation', 8 + contentComponent: MapCard, 9 + sidebarButtonText: 'map', 10 + createNew: (item) => { 11 + item.w = 2; 12 + item.h = 2; 13 + item.mobileH = 4; 14 + item.mobileW = 4; 15 + }, 16 + 17 + sidebarComponent: SidebarItemMapCard, 18 + creationModalComponent: CreateMapCardModal 19 + } as CardDefinition & { type: 'mapLocation' };
+2 -2
src/lib/cards/YoutubeVideo/SidebarItemYoutubeCard.svelte
··· 4 4 let { onclick }: { onclick: () => void } = $props(); 5 5 </script> 6 6 7 - <Button {onclick} variant="ghost" class="w-full" size="lg"> 8 - <svg xmlns="http://www.w3.org/2000/svg" class="text-accent-500 h-4" viewBox="0 0 256 180" 7 + <Button {onclick} variant="ghost" class="w-full justify-start"> 8 + <svg xmlns="http://www.w3.org/2000/svg" class="text-accent-600 dark:text-accent-400 h-4" viewBox="0 0 256 180" 9 9 ><path 10 10 fill="currentColor" 11 11 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"
+3 -1
src/lib/cards/index.ts
··· 3 3 import { ImageCardDefinition } from './ImageCard'; 4 4 import { LinkCardDefinition } from './LinkCard'; 5 5 import { LivestreamCardDefitition, LivestreamEmbedCardDefitition } from './LivestreamCard'; 6 + import { MapCardDefinition } from './MapCard'; 6 7 import { UpdatedBlentosCardDefitition } from './SpecialCards/UpdatedBlentos'; 7 8 import { TextCardDefinition } from './TextCard'; 8 9 import type { CardDefinition } from './types'; ··· 17 18 BlueskyPostCardDefinition, 18 19 LivestreamCardDefitition, 19 20 LivestreamEmbedCardDefitition, 20 - EmbedCardDefinition 21 + EmbedCardDefinition, 22 + MapCardDefinition 21 23 ] as const; 22 24 23 25 export const CardDefinitionsByType = AllCardDefinitions.reduce(
+1
src/lib/cards/types.ts
··· 35 35 upload?: (item: Item) => Promise<Item>; 36 36 37 37 sidebarComponent?: Component<SidebarComponentProps>; 38 + sidebarButtonText?: string; 38 39 };
+20
src/routes/api/geocoding/+server.ts
··· 1 + import { json } from '@sveltejs/kit'; 2 + 3 + export async function GET({ url }) { 4 + const q = url.searchParams.get('q'); 5 + if (!q) { 6 + return json({ error: 'No search provided' }, { status: 400 }); 7 + } 8 + 9 + try { 10 + const data = await fetch( 11 + 'https://nominatim.openstreetmap.org/search?format=json&q=' + encodeURIComponent(q) 12 + ); 13 + const location = await data.json(); 14 + 15 + return json(location[0]); 16 + } catch (error) { 17 + console.error('Error fetching location:', error); 18 + return json({ error: 'Failed to fetch location' }, { status: 500 }); 19 + } 20 + }