tangled
alpha
login
or
join now
flo-bit.dev
/
blento
21
fork
atom
your personal website on atproto - mirror
blento.app
21
fork
atom
overview
issues
pulls
pipelines
add map location card
Florian
2 months ago
0c4fcf75
c96a01b5
+242
-17
16 changed files
expand all
collapse all
unified
split
package.json
pnpm-lock.yaml
src
lib
EditableWebsite.svelte
cards
BlueskyPostCard
SidebarItemBlueskyPostCard.svelte
EmbedCard
SidebarItemEmbedCard.svelte
LivestreamCard
SidebarItemEmbedLivestreamCard.svelte
SidebarItemLivestreamCard.svelte
MapCard
CreateMapCardModal.svelte
Map.svelte
MapCard.svelte
SidebarItemMapCard.svelte
index.ts
YoutubeVideo
SidebarItemYoutubeCard.svelte
index.ts
types.ts
routes
api
geocoding
+server.ts
+1
package.json
···
61
61
"bits-ui": "^2.14.4",
62
62
"clsx": "^2.1.1",
63
63
"gsap": "^3.14.2",
64
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
71
+
leaflet:
72
72
+
specifier: ^1.9.4
73
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
1893
+
1894
1894
+
leaflet@1.9.4:
1895
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
4445
+
4446
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
178
-
const sidebarItems = AllCardDefinitions.filter((cardDef) => cardDef.sidebarComponent);
178
178
+
const sidebarItems = AllCardDefinitions.filter(
179
179
+
(cardDef) => cardDef.sidebarComponent || cardDef.sidebarButtonText
180
180
+
);
179
181
</script>
180
182
181
183
{#if !dev}
···
328
330
</div>
329
331
</div>
330
332
331
331
-
<Sidebar mobileOnly mobileClasses="lg:block p-4">
332
332
-
<div>
333
333
+
<Sidebar mobileOnly mobileClasses="lg:block p-4 gap-4">
334
334
+
<div class="flex flex-col gap-2">
333
335
{#each sidebarItems as cardDef}
334
334
-
<cardDef.sidebarComponent onclick={() => newCard(cardDef.type)} />
336
336
+
{#if cardDef.sidebarComponent}
337
337
+
<cardDef.sidebarComponent onclick={() => newCard(cardDef.type)} />
338
338
+
{:else if cardDef.sidebarButtonText}
339
339
+
<Button onclick={() => newCard(cardDef.type)} variant="ghost" class="w-full justify-start"
340
340
+
>{cardDef.sidebarButtonText}</Button
341
341
+
>
342
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
7
-
<Button {onclick} variant="ghost" class="w-full" size="lg">
7
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
7
-
<Button {onclick} variant="ghost" class="w-full" size="lg">
7
7
+
<Button {onclick} variant="ghost" class="w-full justify-start">
8
8
<svg
9
9
xmlns="http://www.w3.org/2000/svg"
10
10
-
fill="none"
11
10
viewBox="0 0 24 24"
12
12
-
stroke-width="1.5"
13
13
-
stroke="currentColor"
11
11
+
fill="currentColor"
14
12
class="text-accent-600 dark:text-accent-400"
15
13
>
16
14
<path
17
17
-
stroke-linecap="round"
18
18
-
stroke-linejoin="round"
19
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
15
+
fill-rule="evenodd"
16
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
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
8
-
<Button {onclick} variant="ghost" class="w-full" size="lg">
8
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
8
-
<Button {onclick} variant="ghost" class="w-full" size="lg">
8
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
1
+
<script lang="ts">
2
2
+
import { Alert, Button, Input, Modal, Subheading } from '@foxui/core';
3
3
+
import type { CreationModalComponentProps } from '../types';
4
4
+
5
5
+
let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props();
6
6
+
7
7
+
let isFetchingLocation = $state(false);
8
8
+
9
9
+
let errorMessage = $state('');
10
10
+
11
11
+
let search = $state('');
12
12
+
13
13
+
async function fetchLocation() {
14
14
+
errorMessage = '';
15
15
+
isFetchingLocation = true;
16
16
+
17
17
+
try {
18
18
+
const response = await fetch('/api/geocoding?q=' + encodeURIComponent(search));
19
19
+
if (response.ok) {
20
20
+
const data = await response.json();
21
21
+
22
22
+
if (!data.lat || !data.lon) throw new Error('lat or lon not found');
23
23
+
24
24
+
item.cardData.lat = parseFloat(data.lat);
25
25
+
item.cardData.lon = parseFloat(data.lon);
26
26
+
} else {
27
27
+
throw new Error('response not ok');
28
28
+
}
29
29
+
} catch (error) {
30
30
+
errorMessage = "Couldn't find that location!";
31
31
+
return false;
32
32
+
} finally {
33
33
+
isFetchingLocation = false;
34
34
+
}
35
35
+
return true;
36
36
+
}
37
37
+
</script>
38
38
+
39
39
+
<Modal open={true} closeButton={false}>
40
40
+
<Subheading>Enter a city and country</Subheading>
41
41
+
<Input bind:value={search} />
42
42
+
43
43
+
{#if errorMessage}
44
44
+
<Alert type="error" title="Failed to create map card"><span>{errorMessage}</span></Alert>
45
45
+
{/if}
46
46
+
47
47
+
<div class="mt-4 flex justify-end gap-2">
48
48
+
<Button onclick={oncancel} variant="ghost">Cancel</Button>
49
49
+
<Button
50
50
+
disabled={isFetchingLocation}
51
51
+
onclick={async () => {
52
52
+
if (await fetchLocation()) oncreate();
53
53
+
}}>{isFetchingLocation ? 'Creating...' : 'Create'}</Button
54
54
+
>
55
55
+
</div>
56
56
+
</Modal>
+80
src/lib/cards/MapCard/Map.svelte
···
1
1
+
<script lang="ts">
2
2
+
import { onMount } from 'svelte';
3
3
+
import 'leaflet/dist/leaflet.css';
4
4
+
import type { Item } from '$lib/types';
5
5
+
6
6
+
let { item }: { item: Item } = $props();
7
7
+
8
8
+
let lMap;
9
9
+
let leaflet;
10
10
+
11
11
+
let map: HTMLElement | undefined = $state();
12
12
+
13
13
+
function getCSSVar(variable: string) {
14
14
+
return getComputedStyle(document.body).getPropertyValue(variable).trim();
15
15
+
}
16
16
+
17
17
+
onMount(async () => {
18
18
+
try {
19
19
+
console.log(`Loading map...`);
20
20
+
21
21
+
// @ts-ignore
22
22
+
leaflet = await import('leaflet');
23
23
+
24
24
+
const location = [item.cardData.lat, item.cardData.lon];
25
25
+
26
26
+
lMap = leaflet
27
27
+
.map(map, {
28
28
+
zoomControl: false,
29
29
+
dragging: false,
30
30
+
minZoom: 2,
31
31
+
maxZoom: 5
32
32
+
})
33
33
+
.setView(location, 3.5);
34
34
+
leaflet
35
35
+
.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
36
36
+
attribution:
37
37
+
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
38
38
+
})
39
39
+
.addTo(lMap);
40
40
+
41
41
+
let color =
42
42
+
!item.color || item.color === 'transparent' || item.color === 'base'
43
43
+
? 'accent'
44
44
+
: item.color;
45
45
+
46
46
+
console.log(color);
47
47
+
48
48
+
const computedColor = getCSSVar(`--color-${color}-500`);
49
49
+
50
50
+
leaflet
51
51
+
.circle(location, {
52
52
+
color: computedColor,
53
53
+
fillColor: computedColor,
54
54
+
fillOpacity: 0.5,
55
55
+
radius: 100000,
56
56
+
class: '!grayscale-0'
57
57
+
})
58
58
+
.addTo(lMap);
59
59
+
} catch (err) {
60
60
+
console.error(`Something went wrong trying to get the geolocation data`, err);
61
61
+
}
62
62
+
});
63
63
+
</script>
64
64
+
65
65
+
<div
66
66
+
bind:this={map}
67
67
+
class={[
68
68
+
'absolute inset-0 isolate h-full w-full pointer-coarse:pointer-events-none',
69
69
+
item.color && item.color !== 'base' && item.color !== 'transparent' ? 'mix-blend-multiply' : ''
70
70
+
]}
71
71
+
></div>
72
72
+
73
73
+
<style>
74
74
+
:global(:not(.dark)) :global(.leaflet-layer) {
75
75
+
filter: grayscale(100%);
76
76
+
}
77
77
+
:global(.dark) :global(.leaflet-layer) {
78
78
+
filter: grayscale(100%) invert(100%);
79
79
+
}
80
80
+
</style>
+10
src/lib/cards/MapCard/MapCard.svelte
···
1
1
+
<script lang="ts">
2
2
+
import type { Item } from '$lib/types';
3
3
+
import Map from './Map.svelte';
4
4
+
5
5
+
let { item }: { item: Item } = $props();
6
6
+
</script>
7
7
+
8
8
+
{#key item.color}
9
9
+
<Map {item} />
10
10
+
{/key}
+22
src/lib/cards/MapCard/SidebarItemMapCard.svelte
···
1
1
+
<script lang="ts">
2
2
+
import { Button } from '@foxui/core';
3
3
+
4
4
+
let { onclick }: { onclick: () => void } = $props();
5
5
+
</script>
6
6
+
7
7
+
<Button {onclick} variant="ghost" class="w-full justify-start">
8
8
+
<svg
9
9
+
xmlns="http://www.w3.org/2000/svg"
10
10
+
viewBox="0 0 24 24"
11
11
+
fill="currentColor"
12
12
+
class="text-accent-600 dark:text-accent-400"
13
13
+
>
14
14
+
<path
15
15
+
fill-rule="evenodd"
16
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
17
+
clip-rule="evenodd"
18
18
+
/>
19
19
+
</svg>
20
20
+
21
21
+
Map with Location
22
22
+
</Button>
+19
src/lib/cards/MapCard/index.ts
···
1
1
+
import type { CardDefinition } from '../types';
2
2
+
import CreateMapCardModal from './CreateMapCardModal.svelte';
3
3
+
import MapCard from './MapCard.svelte';
4
4
+
import SidebarItemMapCard from './SidebarItemMapCard.svelte';
5
5
+
6
6
+
export const MapCardDefinition = {
7
7
+
type: 'mapLocation',
8
8
+
contentComponent: MapCard,
9
9
+
sidebarButtonText: 'map',
10
10
+
createNew: (item) => {
11
11
+
item.w = 2;
12
12
+
item.h = 2;
13
13
+
item.mobileH = 4;
14
14
+
item.mobileW = 4;
15
15
+
},
16
16
+
17
17
+
sidebarComponent: SidebarItemMapCard,
18
18
+
creationModalComponent: CreateMapCardModal
19
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
7
-
<Button {onclick} variant="ghost" class="w-full" size="lg">
8
8
-
<svg xmlns="http://www.w3.org/2000/svg" class="text-accent-500 h-4" viewBox="0 0 256 180"
7
7
+
<Button {onclick} variant="ghost" class="w-full justify-start">
8
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
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
20
-
EmbedCardDefinition
21
21
+
EmbedCardDefinition,
22
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
38
+
sidebarButtonText?: string;
38
39
};
+20
src/routes/api/geocoding/+server.ts
···
1
1
+
import { json } from '@sveltejs/kit';
2
2
+
3
3
+
export async function GET({ url }) {
4
4
+
const q = url.searchParams.get('q');
5
5
+
if (!q) {
6
6
+
return json({ error: 'No search provided' }, { status: 400 });
7
7
+
}
8
8
+
9
9
+
try {
10
10
+
const data = await fetch(
11
11
+
'https://nominatim.openstreetmap.org/search?format=json&q=' + encodeURIComponent(q)
12
12
+
);
13
13
+
const location = await data.json();
14
14
+
15
15
+
return json(location[0]);
16
16
+
} catch (error) {
17
17
+
console.error('Error fetching location:', error);
18
18
+
return json({ error: 'Failed to fetch location' }, { status: 500 });
19
19
+
}
20
20
+
}