@atmo-dev/events-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DatePicker.svelte +231 -0
- package/dist/DatePicker.svelte.d.ts +11 -0
- package/dist/DateTimePicker.svelte +101 -0
- package/dist/DateTimePicker.svelte.d.ts +9 -0
- package/dist/EventAttendees.svelte +203 -0
- package/dist/EventAttendees.svelte.d.ts +13 -0
- package/dist/EventCard.svelte +131 -0
- package/dist/EventCard.svelte.d.ts +8 -0
- package/dist/EventComments.svelte +99 -0
- package/dist/EventComments.svelte.d.ts +6 -0
- package/dist/EventEditor.svelte +589 -0
- package/dist/EventEditor.svelte.d.ts +20 -0
- package/dist/EventRsvp.svelte +237 -0
- package/dist/EventRsvp.svelte.d.ts +17 -0
- package/dist/EventView.svelte +433 -0
- package/dist/EventView.svelte.d.ts +16 -0
- package/dist/ImageDropper.svelte +66 -0
- package/dist/ImageDropper.svelte.d.ts +7 -0
- package/dist/Map.svelte +27 -0
- package/dist/Map.svelte.d.ts +8 -0
- package/dist/PostToBlueskyModal.svelte +244 -0
- package/dist/PostToBlueskyModal.svelte.d.ts +22 -0
- package/dist/ShareModal.svelte +160 -0
- package/dist/ShareModal.svelte.d.ts +23 -0
- package/dist/ThemeApply.svelte +50 -0
- package/dist/ThemeApply.svelte.d.ts +7 -0
- package/dist/ThemeBackground.svelte +33 -0
- package/dist/ThemeBackground.svelte.d.ts +7 -0
- package/dist/ThemePicker.svelte +102 -0
- package/dist/ThemePicker.svelte.d.ts +7 -0
- package/dist/ThumbnailPresets.svelte +68 -0
- package/dist/ThumbnailPresets.svelte.d.ts +11 -0
- package/dist/TimePicker.svelte +188 -0
- package/dist/TimePicker.svelte.d.ts +9 -0
- package/dist/TimezonePicker.svelte +132 -0
- package/dist/TimezonePicker.svelte.d.ts +6 -0
- package/dist/VodPlayer.svelte +137 -0
- package/dist/VodPlayer.svelte.d.ts +14 -0
- package/dist/VodTranscript.svelte +72 -0
- package/dist/VodTranscript.svelte.d.ts +8 -0
- package/dist/atproto-helpers.d.ts +21 -0
- package/dist/atproto-helpers.js +61 -0
- package/dist/cal/helper.d.ts +1 -0
- package/dist/cal/helper.js +20 -0
- package/dist/cal/ical.d.ts +22 -0
- package/dist/cal/ical.js +188 -0
- package/dist/cal/sanitize.d.ts +3 -0
- package/dist/cal/sanitize.js +25 -0
- package/dist/contrail.d.ts +54 -0
- package/dist/contrail.js +22 -0
- package/dist/date-format.d.ts +22 -0
- package/dist/date-format.js +43 -0
- package/dist/editor/LinksSection.svelte +144 -0
- package/dist/editor/LinksSection.svelte.d.ts +10 -0
- package/dist/editor/LocationSection.svelte +215 -0
- package/dist/editor/LocationSection.svelte.d.ts +8 -0
- package/dist/editor/RecurringModal.svelte +270 -0
- package/dist/editor/RecurringModal.svelte.d.ts +30 -0
- package/dist/editor/ThemeSection.svelte +39 -0
- package/dist/editor/ThemeSection.svelte.d.ts +7 -0
- package/dist/editor/ThumbnailSection.svelte +219 -0
- package/dist/editor/ThumbnailSection.svelte.d.ts +13 -0
- package/dist/editor/adapter.d.ts +98 -0
- package/dist/editor/adapter.js +9 -0
- package/dist/editor/save.d.ts +42 -0
- package/dist/editor/save.js +154 -0
- package/dist/editor/types.d.ts +39 -0
- package/dist/editor/types.js +9 -0
- package/dist/event-types.d.ts +70 -0
- package/dist/event-types.js +11 -0
- package/dist/event-view/AddToCalendarButton.svelte +42 -0
- package/dist/event-view/AddToCalendarButton.svelte.d.ts +9 -0
- package/dist/event-view/EventBadges.svelte +20 -0
- package/dist/event-view/EventBadges.svelte.d.ts +7 -0
- package/dist/event-view/EventDateBlock.svelte +43 -0
- package/dist/event-view/EventDateBlock.svelte.d.ts +7 -0
- package/dist/event-view/EventHostedBy.svelte +63 -0
- package/dist/event-view/EventHostedBy.svelte.d.ts +16 -0
- package/dist/event-view/EventLinksList.svelte +37 -0
- package/dist/event-view/EventLinksList.svelte.d.ts +9 -0
- package/dist/event-view/EventLocationBlock.svelte +48 -0
- package/dist/event-view/EventLocationBlock.svelte.d.ts +7 -0
- package/dist/event-view/EventLocationMap.svelte +72 -0
- package/dist/event-view/EventLocationMap.svelte.d.ts +8 -0
- package/dist/event-view/ExternalRsvpNotice.svelte +44 -0
- package/dist/event-view/ExternalRsvpNotice.svelte.d.ts +6 -0
- package/dist/event-view/InviteShareFlow.svelte +177 -0
- package/dist/event-view/InviteShareFlow.svelte.d.ts +15 -0
- package/dist/event-view/StreamPlacePlayer.svelte +222 -0
- package/dist/event-view/StreamPlacePlayer.svelte.d.ts +8 -0
- package/dist/event-view/format.d.ts +26 -0
- package/dist/event-view/format.js +145 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/profile-url.d.ts +1 -0
- package/dist/profile-url.js +7 -0
- package/dist/theme.d.ts +9 -0
- package/dist/theme.js +22 -0
- package/dist/themes/Blobs.svelte +35 -0
- package/dist/themes/Blobs.svelte.d.ts +26 -0
- package/dist/themes/Butterflies.svelte +185 -0
- package/dist/themes/Butterflies.svelte.d.ts +3 -0
- package/dist/themes/Fireflies.svelte +134 -0
- package/dist/themes/Fireflies.svelte.d.ts +3 -0
- package/dist/themes/Kaleidoscope.svelte +177 -0
- package/dist/themes/Kaleidoscope.svelte.d.ts +3 -0
- package/dist/themes/Matrix.svelte +150 -0
- package/dist/themes/Matrix.svelte.d.ts +3 -0
- package/dist/themes/Stars.svelte +98 -0
- package/dist/themes/Stars.svelte.d.ts +3 -0
- package/dist/thumbnails/designs.d.ts +18 -0
- package/dist/thumbnails/designs.js +316 -0
- package/package.json +95 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
data: any;
|
|
4
|
+
adapter: EditorAdapter;
|
|
5
|
+
viewer: EditorViewer;
|
|
6
|
+
/** Current page URL — used for the OG image link and the calendar button. */
|
|
7
|
+
pageUrl: URL;
|
|
8
|
+
embedMode?: boolean;
|
|
9
|
+
/** When set, the share modal / Bluesky post embed use this URL instead
|
|
10
|
+
* of the canonical atmo.rsvp event URL. Useful for embedders that want
|
|
11
|
+
* share links to point at their own event page. */
|
|
12
|
+
shareUrlOverride?: string;
|
|
13
|
+
};
|
|
14
|
+
declare const EventView: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
15
|
+
type EventView = ReturnType<typeof EventView>;
|
|
16
|
+
export default EventView;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Portal } from 'bits-ui';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
processImageFile,
|
|
6
|
+
isDragOver = $bindable()
|
|
7
|
+
}: {
|
|
8
|
+
processImageFile: (file: File) => Promise<void>;
|
|
9
|
+
isDragOver: boolean;
|
|
10
|
+
} = $props();
|
|
11
|
+
|
|
12
|
+
function handleDragOver(event: DragEvent) {
|
|
13
|
+
event.preventDefault();
|
|
14
|
+
event.stopPropagation();
|
|
15
|
+
|
|
16
|
+
const dt = event.dataTransfer;
|
|
17
|
+
if (!dt) return;
|
|
18
|
+
|
|
19
|
+
let imageCount = 0;
|
|
20
|
+
if (dt.items) {
|
|
21
|
+
for (let i = 0; i < dt.items.length; i++) {
|
|
22
|
+
const item = dt.items[i];
|
|
23
|
+
if (item && item.kind === 'file' && item.type.startsWith('image/')) {
|
|
24
|
+
imageCount++;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} else if (dt.files) {
|
|
28
|
+
for (let i = 0; i < dt.files.length; i++) {
|
|
29
|
+
const file = dt.files[i];
|
|
30
|
+
if (file?.type.startsWith('image/')) {
|
|
31
|
+
imageCount++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isDragOver = imageCount > 0;
|
|
37
|
+
}
|
|
38
|
+
function handleDragLeave(event: DragEvent) {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
event.stopPropagation();
|
|
41
|
+
isDragOver = false;
|
|
42
|
+
}
|
|
43
|
+
async function handleDrop(event: DragEvent) {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
event.stopPropagation();
|
|
46
|
+
isDragOver = false;
|
|
47
|
+
if (!event.dataTransfer?.files?.length) return;
|
|
48
|
+
for (const file of event.dataTransfer.files) {
|
|
49
|
+
if (file?.type.startsWith('image/')) {
|
|
50
|
+
await processImageFile(file);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<svelte:window ondragover={handleDragOver} ondragleave={handleDragLeave} ondrop={handleDrop} />
|
|
57
|
+
|
|
58
|
+
{#if isDragOver}
|
|
59
|
+
<Portal>
|
|
60
|
+
<div
|
|
61
|
+
class="bg-base-100/80 dark:bg-base-900/80 text-primary dark:text-base-100 pointer-events-none absolute inset-0 z-[1000] flex items-center justify-center text-4xl font-bold backdrop-blur-md"
|
|
62
|
+
>
|
|
63
|
+
Drop file to add it to your message
|
|
64
|
+
</div>
|
|
65
|
+
</Portal>
|
|
66
|
+
{/if}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
processImageFile: (file: File) => Promise<void>;
|
|
3
|
+
isDragOver: boolean;
|
|
4
|
+
};
|
|
5
|
+
declare const ImageDropper: import("svelte").Component<$$ComponentProps, {}, "isDragOver">;
|
|
6
|
+
type ImageDropper = ReturnType<typeof ImageDropper>;
|
|
7
|
+
export default ImageDropper;
|
package/dist/Map.svelte
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { MapLibre, Projection, Marker, AttributionControl } from 'svelte-maplibre-gl';
|
|
3
|
+
import maplibregl from 'maplibre-gl';
|
|
4
|
+
|
|
5
|
+
let { lat, lng, zoom = 11 }: { lat: number; lng: number; zoom?: number } = $props();
|
|
6
|
+
|
|
7
|
+
let map: maplibregl.Map | undefined = $state();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<MapLibre
|
|
11
|
+
bind:map
|
|
12
|
+
class="h-full w-full overflow-hidden rounded-xl"
|
|
13
|
+
style="https://tiles.openfreemap.org/styles/liberty"
|
|
14
|
+
{zoom}
|
|
15
|
+
center={[lng, lat]}
|
|
16
|
+
attributionControl={false}
|
|
17
|
+
>
|
|
18
|
+
<AttributionControl position="bottom-left" compact={false} />
|
|
19
|
+
<Projection type="globe" />
|
|
20
|
+
<Marker lnglat={[lng, lat]}>
|
|
21
|
+
{#snippet content()}
|
|
22
|
+
<div class="from-accent-400 size-10 rounded-full bg-radial via-transparent p-3">
|
|
23
|
+
<div class="bg-accent-500 size-4 rounded-full ring-2 ring-white"></div>
|
|
24
|
+
</div>
|
|
25
|
+
{/snippet}
|
|
26
|
+
</Marker>
|
|
27
|
+
</MapLibre>
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Modal, Button, Checkbox, Label } from '@foxui/core';
|
|
3
|
+
import {
|
|
4
|
+
MicrobloggingPostCreator,
|
|
5
|
+
editorJsonToBlueskyPost,
|
|
6
|
+
createBlueskyMentionSearch,
|
|
7
|
+
LinkCard,
|
|
8
|
+
type MicrobloggingPostContent
|
|
9
|
+
} from '@foxui/social';
|
|
10
|
+
import type { JSONContent, SvelteTiptap } from '@foxui/text';
|
|
11
|
+
import type { Readable } from 'svelte/store';
|
|
12
|
+
import { get } from 'svelte/store';
|
|
13
|
+
import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
open = $bindable(false),
|
|
17
|
+
canSetEventComments = false,
|
|
18
|
+
eventDid,
|
|
19
|
+
eventRkey,
|
|
20
|
+
eventName,
|
|
21
|
+
eventUrl,
|
|
22
|
+
eventDescription,
|
|
23
|
+
ogImageUrl,
|
|
24
|
+
initialText,
|
|
25
|
+
adapter,
|
|
26
|
+
viewer,
|
|
27
|
+
onPosted
|
|
28
|
+
}: {
|
|
29
|
+
open: boolean;
|
|
30
|
+
canSetEventComments?: boolean;
|
|
31
|
+
eventDid: string;
|
|
32
|
+
eventRkey: string;
|
|
33
|
+
eventName: string;
|
|
34
|
+
eventUrl: string;
|
|
35
|
+
eventDescription?: string;
|
|
36
|
+
ogImageUrl?: string;
|
|
37
|
+
initialText: string;
|
|
38
|
+
adapter: EditorAdapter;
|
|
39
|
+
viewer: EditorViewer;
|
|
40
|
+
onPosted?: (ref: { uri: string; cid: string; showComments: boolean }) => void;
|
|
41
|
+
} = $props();
|
|
42
|
+
|
|
43
|
+
function textToDoc(text: string): JSONContent {
|
|
44
|
+
const lines = text.split('\n');
|
|
45
|
+
const content = lines.map((line) =>
|
|
46
|
+
line.length > 0
|
|
47
|
+
? { type: 'paragraph', content: [{ type: 'text', text: line }] }
|
|
48
|
+
: { type: 'paragraph' }
|
|
49
|
+
);
|
|
50
|
+
return { type: 'doc', content } as JSONContent;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const searchMentions = createBlueskyMentionSearch();
|
|
54
|
+
|
|
55
|
+
// Seed from the initial prop; the $effect below re-pushes it into the editor
|
|
56
|
+
// each time the modal opens, so only capturing the initial value is intentional.
|
|
57
|
+
// svelte-ignore state_referenced_locally
|
|
58
|
+
let postContent = $state<MicrobloggingPostContent>({
|
|
59
|
+
text: initialText,
|
|
60
|
+
json: textToDoc(initialText)
|
|
61
|
+
});
|
|
62
|
+
let editorStore = $state<Readable<SvelteTiptap.Editor> | undefined>();
|
|
63
|
+
let prefilledForOpen = $state(false);
|
|
64
|
+
let showComments = $state(true);
|
|
65
|
+
let posting = $state(false);
|
|
66
|
+
let errorMessage = $state<string | null>(null);
|
|
67
|
+
|
|
68
|
+
// Each time the modal opens, push the initial text into the editor once it's
|
|
69
|
+
// ready. The editor instance arrives via a readable store after PlainTextEditor
|
|
70
|
+
// has mounted internally, so we subscribe and write content on the first
|
|
71
|
+
// non-null value we see for this open cycle.
|
|
72
|
+
$effect(() => {
|
|
73
|
+
if (!open) {
|
|
74
|
+
prefilledForOpen = false;
|
|
75
|
+
showComments = true;
|
|
76
|
+
errorMessage = null;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (!editorStore || prefilledForOpen) return;
|
|
80
|
+
const unsub = editorStore.subscribe((ed) => {
|
|
81
|
+
if (!ed || prefilledForOpen) return;
|
|
82
|
+
ed.commands.setContent(textToDoc(initialText));
|
|
83
|
+
prefilledForOpen = true;
|
|
84
|
+
});
|
|
85
|
+
return unsub;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Bluesky's external embed accepts a thumb blob up to ~1MB. Fetch the OG
|
|
89
|
+
// image, upload it to the user's PDS, return a clean blob ref. On any failure
|
|
90
|
+
// (CORS, large image, network) we fall back to a thumb-less embed rather than
|
|
91
|
+
// blocking the post.
|
|
92
|
+
async function fetchAndUploadThumbnail(url: string) {
|
|
93
|
+
try {
|
|
94
|
+
const resp = await fetch(url);
|
|
95
|
+
if (!resp.ok) return null;
|
|
96
|
+
const blob = await resp.blob();
|
|
97
|
+
if (!blob.type.startsWith('image/')) return null;
|
|
98
|
+
if (blob.size > 1_000_000) return null;
|
|
99
|
+
const result = await adapter.uploadBlob(blob);
|
|
100
|
+
return {
|
|
101
|
+
$type: result.$type,
|
|
102
|
+
ref: result.ref,
|
|
103
|
+
mimeType: result.mimeType,
|
|
104
|
+
size: result.size
|
|
105
|
+
};
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.warn('PostToBlueskyModal: thumbnail upload failed, posting without thumb', err);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function handlePost() {
|
|
113
|
+
if (!viewer.did || posting) return;
|
|
114
|
+
|
|
115
|
+
// Read the editor's live JSON directly — postContent only updates on the
|
|
116
|
+
// editor's onupdate callback and can lag behind setContent prefills.
|
|
117
|
+
const editor = editorStore ? get(editorStore) : undefined;
|
|
118
|
+
const liveJson: JSONContent = editor
|
|
119
|
+
? (editor.getJSON() as JSONContent)
|
|
120
|
+
: postContent.json;
|
|
121
|
+
|
|
122
|
+
posting = true;
|
|
123
|
+
errorMessage = null;
|
|
124
|
+
try {
|
|
125
|
+
const { text, facets } = editorJsonToBlueskyPost(liveJson);
|
|
126
|
+
|
|
127
|
+
if (!text.trim()) {
|
|
128
|
+
errorMessage = 'Post text cannot be empty';
|
|
129
|
+
posting = false;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const externalEmbed: Record<string, unknown> = {
|
|
134
|
+
uri: eventUrl,
|
|
135
|
+
title: eventName,
|
|
136
|
+
description: eventDescription ?? ''
|
|
137
|
+
};
|
|
138
|
+
if (ogImageUrl) {
|
|
139
|
+
const thumb = await fetchAndUploadThumbnail(ogImageUrl);
|
|
140
|
+
if (thumb) externalEmbed.thumb = thumb;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const postRecord: Record<string, unknown> = {
|
|
144
|
+
$type: 'app.bsky.feed.post',
|
|
145
|
+
text,
|
|
146
|
+
createdAt: new Date().toISOString(),
|
|
147
|
+
embed: {
|
|
148
|
+
$type: 'app.bsky.embed.external',
|
|
149
|
+
external: externalEmbed
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
if (facets.length > 0) postRecord.facets = facets;
|
|
153
|
+
|
|
154
|
+
const postResp = await adapter.createRecord({
|
|
155
|
+
collection: 'app.bsky.feed.post',
|
|
156
|
+
record: postRecord
|
|
157
|
+
});
|
|
158
|
+
if (!postResp.uri || !postResp.cid) {
|
|
159
|
+
console.error('PostToBlueskyModal: PDS response missing uri/cid', postResp);
|
|
160
|
+
throw new Error(
|
|
161
|
+
'PDS rejected the post — try logging out and back in to refresh permissions'
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
const postUri = postResp.uri;
|
|
165
|
+
const postCid = postResp.cid;
|
|
166
|
+
|
|
167
|
+
if (canSetEventComments) {
|
|
168
|
+
const fresh = await adapter.getRecord({
|
|
169
|
+
did: eventDid,
|
|
170
|
+
collection: 'community.lexicon.calendar.event',
|
|
171
|
+
rkey: eventRkey
|
|
172
|
+
});
|
|
173
|
+
const updatedRecord = {
|
|
174
|
+
...fresh.value,
|
|
175
|
+
bskyPostRef: {
|
|
176
|
+
uri: postUri,
|
|
177
|
+
cid: postCid,
|
|
178
|
+
showComments
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
await adapter.putRecord({
|
|
182
|
+
collection: 'community.lexicon.calendar.event',
|
|
183
|
+
rkey: eventRkey,
|
|
184
|
+
record: updatedRecord
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await adapter.notifyUpdate?.(
|
|
188
|
+
`at://${eventDid}/community.lexicon.calendar.event/${eventRkey}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
onPosted?.({ uri: postUri, cid: postCid, showComments });
|
|
193
|
+
open = false;
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error('PostToBlueskyModal: post failed', err);
|
|
196
|
+
errorMessage = err instanceof Error ? err.message : 'Failed to post';
|
|
197
|
+
} finally {
|
|
198
|
+
posting = false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
</script>
|
|
202
|
+
|
|
203
|
+
<Modal bind:open>
|
|
204
|
+
<div class="space-y-4">
|
|
205
|
+
<h2 class="text-base-900 dark:text-base-50 text-xl font-bold">Share to Bluesky</h2>
|
|
206
|
+
|
|
207
|
+
<div
|
|
208
|
+
class="border-base-200 dark:border-base-800 bg-base-50 dark:bg-base-950/30 space-y-3 rounded-xl border p-3"
|
|
209
|
+
>
|
|
210
|
+
<MicrobloggingPostCreator
|
|
211
|
+
bind:editor={editorStore}
|
|
212
|
+
bind:content={postContent}
|
|
213
|
+
{searchMentions}
|
|
214
|
+
maxLength={300}
|
|
215
|
+
textEditorClass="max-h-48 overflow-y-auto"
|
|
216
|
+
/>
|
|
217
|
+
<LinkCard
|
|
218
|
+
href={eventUrl}
|
|
219
|
+
meta={{
|
|
220
|
+
title: eventName,
|
|
221
|
+
description: eventDescription,
|
|
222
|
+
image: ogImageUrl
|
|
223
|
+
}}
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
{#if canSetEventComments}
|
|
228
|
+
<Label class="flex items-center gap-2">
|
|
229
|
+
<Checkbox bind:checked={showComments} />
|
|
230
|
+
<span class="text-base-700 dark:text-base-300 text-sm">
|
|
231
|
+
Show comments on event page
|
|
232
|
+
</span>
|
|
233
|
+
</Label>
|
|
234
|
+
{/if}
|
|
235
|
+
|
|
236
|
+
{#if errorMessage}
|
|
237
|
+
<p class="text-red-600 dark:text-red-400 text-sm">{errorMessage}</p>
|
|
238
|
+
{/if}
|
|
239
|
+
|
|
240
|
+
<Button class="w-full" onclick={handlePost} disabled={posting}>
|
|
241
|
+
{posting ? 'Posting…' : 'Post'}
|
|
242
|
+
</Button>
|
|
243
|
+
</div>
|
|
244
|
+
</Modal>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
canSetEventComments?: boolean;
|
|
5
|
+
eventDid: string;
|
|
6
|
+
eventRkey: string;
|
|
7
|
+
eventName: string;
|
|
8
|
+
eventUrl: string;
|
|
9
|
+
eventDescription?: string;
|
|
10
|
+
ogImageUrl?: string;
|
|
11
|
+
initialText: string;
|
|
12
|
+
adapter: EditorAdapter;
|
|
13
|
+
viewer: EditorViewer;
|
|
14
|
+
onPosted?: (ref: {
|
|
15
|
+
uri: string;
|
|
16
|
+
cid: string;
|
|
17
|
+
showComments: boolean;
|
|
18
|
+
}) => void;
|
|
19
|
+
};
|
|
20
|
+
declare const PostToBlueskyModal: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
21
|
+
type PostToBlueskyModal = ReturnType<typeof PostToBlueskyModal>;
|
|
22
|
+
export default PostToBlueskyModal;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Modal, Button, Avatar } from '@foxui/core';
|
|
3
|
+
import { LinkCard } from '@foxui/social';
|
|
4
|
+
import PostToBlueskyModal from './PostToBlueskyModal.svelte';
|
|
5
|
+
import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
open = $bindable(false),
|
|
9
|
+
url,
|
|
10
|
+
title = 'Event created!',
|
|
11
|
+
shareText,
|
|
12
|
+
eventName,
|
|
13
|
+
ogImageUrl,
|
|
14
|
+
canSetEventComments = false,
|
|
15
|
+
eventDid,
|
|
16
|
+
eventRkey,
|
|
17
|
+
eventDescription,
|
|
18
|
+
adapter,
|
|
19
|
+
viewer,
|
|
20
|
+
onPosted
|
|
21
|
+
}: {
|
|
22
|
+
open: boolean;
|
|
23
|
+
url: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
shareText?: string;
|
|
26
|
+
eventName?: string;
|
|
27
|
+
ogImageUrl?: string;
|
|
28
|
+
canSetEventComments?: boolean;
|
|
29
|
+
eventDid?: string;
|
|
30
|
+
eventRkey?: string;
|
|
31
|
+
eventDescription?: string;
|
|
32
|
+
adapter: EditorAdapter;
|
|
33
|
+
viewer: EditorViewer;
|
|
34
|
+
onPosted?: (ref: { uri: string; cid: string; showComments: boolean }) => void;
|
|
35
|
+
} = $props();
|
|
36
|
+
|
|
37
|
+
let copiedUrl = $state(false);
|
|
38
|
+
let copiedText = $state(false);
|
|
39
|
+
let showPostModal = $state(false);
|
|
40
|
+
|
|
41
|
+
let textBeforeUrl = $derived(shareText ? shareText.replace(url, '').trim() : undefined);
|
|
42
|
+
|
|
43
|
+
async function copyUrl() {
|
|
44
|
+
try {
|
|
45
|
+
await navigator.clipboard.writeText(url);
|
|
46
|
+
copiedUrl = true;
|
|
47
|
+
setTimeout(() => (copiedUrl = false), 2000);
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function copyText() {
|
|
52
|
+
if (!shareText) return;
|
|
53
|
+
try {
|
|
54
|
+
await navigator.clipboard.writeText(shareText);
|
|
55
|
+
copiedText = true;
|
|
56
|
+
setTimeout(() => (copiedText = false), 2000);
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let blueskyButton: HTMLElement | null = $state(null);
|
|
61
|
+
|
|
62
|
+
let canPostDirectly = $derived(
|
|
63
|
+
!!eventDid && !!eventRkey && !!eventName && viewer.isLoggedIn
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
let blueskyIntentUrl = $derived(
|
|
67
|
+
`https://bsky.app/intent/compose?text=${encodeURIComponent(shareText || url)}`
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
function handleBlueskyClick() {
|
|
71
|
+
showPostModal = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handlePostedFromInner(ref: { uri: string; cid: string; showComments: boolean }) {
|
|
75
|
+
onPosted?.(ref);
|
|
76
|
+
open = false;
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<Modal
|
|
81
|
+
bind:open
|
|
82
|
+
onOpenAutoFocus={(e) => {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
blueskyButton?.focus();
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<div>
|
|
88
|
+
<h2 class="text-base-900 dark:text-base-50 mb-2 text-xl font-bold">{title}</h2>
|
|
89
|
+
<p class="text-base-500 dark:text-base-400 mb-4 text-sm">Share it with others!</p>
|
|
90
|
+
|
|
91
|
+
<div
|
|
92
|
+
class="bg-base-200 dark:bg-base-950/30 border-base-400/40 dark:border-base-700 mb-6 overflow-hidden rounded-xl border px-4 py-3 text-left"
|
|
93
|
+
>
|
|
94
|
+
{#if viewer.isLoggedIn}
|
|
95
|
+
<div class="flex items-center gap-2 pb-4">
|
|
96
|
+
<Avatar src={viewer.avatar} alt="" class="size-6" />
|
|
97
|
+
<span class="text-base-700 dark:text-base-200 text-sm font-medium"
|
|
98
|
+
>{viewer.handle ?? viewer.did}</span
|
|
99
|
+
>
|
|
100
|
+
</div>
|
|
101
|
+
{/if}
|
|
102
|
+
{#if textBeforeUrl}
|
|
103
|
+
<p class="text-base-700 dark:text-base-200 text-md font-semibold">{textBeforeUrl}</p>
|
|
104
|
+
{/if}
|
|
105
|
+
{#if eventName}
|
|
106
|
+
<LinkCard
|
|
107
|
+
href={url}
|
|
108
|
+
meta={{
|
|
109
|
+
title: eventName,
|
|
110
|
+
image: ogImageUrl
|
|
111
|
+
}}
|
|
112
|
+
class="mb-1"
|
|
113
|
+
/>
|
|
114
|
+
{/if}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div class="flex flex-col gap-2 sm:flex-row">
|
|
118
|
+
<Button class="flex-1" variant="secondary" onclick={copyUrl}>
|
|
119
|
+
{copiedUrl ? 'Copied!' : 'Copy link'}
|
|
120
|
+
</Button>
|
|
121
|
+
{#if shareText}
|
|
122
|
+
<Button class="flex-1" variant="secondary" onclick={copyText}>
|
|
123
|
+
{copiedText ? 'Copied!' : 'Copy text'}
|
|
124
|
+
</Button>
|
|
125
|
+
{/if}
|
|
126
|
+
{#if canPostDirectly}
|
|
127
|
+
<Button bind:ref={blueskyButton} class="flex-1" onclick={handleBlueskyClick}>
|
|
128
|
+
Share to Bluesky
|
|
129
|
+
</Button>
|
|
130
|
+
{:else}
|
|
131
|
+
<Button
|
|
132
|
+
bind:ref={blueskyButton}
|
|
133
|
+
class="flex-1"
|
|
134
|
+
href={blueskyIntentUrl}
|
|
135
|
+
target="_blank"
|
|
136
|
+
rel="noopener"
|
|
137
|
+
>
|
|
138
|
+
Share to Bluesky
|
|
139
|
+
</Button>
|
|
140
|
+
{/if}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</Modal>
|
|
144
|
+
|
|
145
|
+
{#if canPostDirectly && eventDid && eventRkey && eventName}
|
|
146
|
+
<PostToBlueskyModal
|
|
147
|
+
bind:open={showPostModal}
|
|
148
|
+
{canSetEventComments}
|
|
149
|
+
{eventDid}
|
|
150
|
+
{eventRkey}
|
|
151
|
+
{eventName}
|
|
152
|
+
eventUrl={url}
|
|
153
|
+
{eventDescription}
|
|
154
|
+
{ogImageUrl}
|
|
155
|
+
initialText={textBeforeUrl ?? eventName}
|
|
156
|
+
{adapter}
|
|
157
|
+
{viewer}
|
|
158
|
+
onPosted={handlePostedFromInner}
|
|
159
|
+
/>
|
|
160
|
+
{/if}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
url: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
shareText?: string;
|
|
7
|
+
eventName?: string;
|
|
8
|
+
ogImageUrl?: string;
|
|
9
|
+
canSetEventComments?: boolean;
|
|
10
|
+
eventDid?: string;
|
|
11
|
+
eventRkey?: string;
|
|
12
|
+
eventDescription?: string;
|
|
13
|
+
adapter: EditorAdapter;
|
|
14
|
+
viewer: EditorViewer;
|
|
15
|
+
onPosted?: (ref: {
|
|
16
|
+
uri: string;
|
|
17
|
+
cid: string;
|
|
18
|
+
showComments: boolean;
|
|
19
|
+
}) => void;
|
|
20
|
+
};
|
|
21
|
+
declare const ShareModal: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
22
|
+
type ShareModal = ReturnType<typeof ShareModal>;
|
|
23
|
+
export default ShareModal;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { BROWSER as browser } from 'esm-env';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
accentColor = 'cyan',
|
|
6
|
+
baseColor = 'mist'
|
|
7
|
+
}: {
|
|
8
|
+
accentColor?: string;
|
|
9
|
+
baseColor?: string;
|
|
10
|
+
} = $props();
|
|
11
|
+
|
|
12
|
+
const allAccentColors = [
|
|
13
|
+
'red', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald',
|
|
14
|
+
'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple',
|
|
15
|
+
'fuchsia', 'pink', 'rose'
|
|
16
|
+
];
|
|
17
|
+
const allBaseColors = [
|
|
18
|
+
'gray', 'stone', 'zinc', 'neutral', 'slate', 'mist', 'sand',
|
|
19
|
+
'olive', 'mauve', 'sage'
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const allColors = [...allAccentColors, ...allBaseColors];
|
|
23
|
+
|
|
24
|
+
const safeJson = (v: string) => JSON.stringify(v).replace(/</g, '\\u003c');
|
|
25
|
+
|
|
26
|
+
// SSR: inline script that removes all color classes then adds the correct ones before paint
|
|
27
|
+
const allColorsJson = JSON.stringify(allColors);
|
|
28
|
+
|
|
29
|
+
let script = $derived(
|
|
30
|
+
`<script>(function(){var e=document.documentElement,r=${allColorsJson};r.forEach(function(c){e.classList.remove(c)});e.classList.add(${safeJson(accentColor)},${safeJson(baseColor)});})()<` +
|
|
31
|
+
'/script>'
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Client: reactive effect for client-side navigations
|
|
35
|
+
$effect(() => {
|
|
36
|
+
if (!browser) return;
|
|
37
|
+
const el = document.documentElement;
|
|
38
|
+
el.classList.remove(...allColors);
|
|
39
|
+
el.classList.add(accentColor, baseColor);
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
el.classList.remove(...allColors);
|
|
43
|
+
el.classList.add('cyan', 'mist');
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<svelte:head>
|
|
49
|
+
{@html script}
|
|
50
|
+
</svelte:head>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { EventTheme } from './theme.js';
|
|
3
|
+
import Blobs from './themes/Blobs.svelte';
|
|
4
|
+
import Stars from './themes/Stars.svelte';
|
|
5
|
+
import Matrix from './themes/Matrix.svelte';
|
|
6
|
+
import Fireflies from './themes/Fireflies.svelte';
|
|
7
|
+
import Butterflies from './themes/Butterflies.svelte';
|
|
8
|
+
import Kaleidoscope from './themes/Kaleidoscope.svelte';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
theme
|
|
12
|
+
}: {
|
|
13
|
+
theme: EventTheme;
|
|
14
|
+
} = $props();
|
|
15
|
+
|
|
16
|
+
let key = $derived(`${theme.name}-${theme.accentColor}-${theme.baseColor}`);
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
{#key key}
|
|
20
|
+
{#if theme.name === 'blobs'}
|
|
21
|
+
<Blobs />
|
|
22
|
+
{:else if theme.name === 'warp'}
|
|
23
|
+
<Stars />
|
|
24
|
+
{:else if theme.name === 'matrix'}
|
|
25
|
+
<Matrix />
|
|
26
|
+
{:else if theme.name === 'fireflies'}
|
|
27
|
+
<Fireflies />
|
|
28
|
+
{:else if theme.name === 'butterflies'}
|
|
29
|
+
<Butterflies />
|
|
30
|
+
{:else if theme.name === 'kaleidoscope'}
|
|
31
|
+
<Kaleidoscope />
|
|
32
|
+
{/if}
|
|
33
|
+
{/key}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { EventTheme } from './theme.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
theme: EventTheme;
|
|
4
|
+
};
|
|
5
|
+
declare const ThemeBackground: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type ThemeBackground = ReturnType<typeof ThemeBackground>;
|
|
7
|
+
export default ThemeBackground;
|