atmo.rsvp
3
fork

Configure Feed

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

change buttons and improve name field in event editor, load more, add recurring events

Florian 8583f070 700ad357

+474 -102
+1
package.json
··· 64 64 "@ethercorps/sveltekit-og": "^4.2.1", 65 65 "@foxui/core": "^0.8.2", 66 66 "@foxui/social": "^0.8.4", 67 + "@foxui/text": "^0.8.2", 67 68 "@foxui/time": "^0.8.2", 68 69 "@foxui/visual": "^0.8.2", 69 70 "@internationalized/date": "^3.12.0",
+3
pnpm-lock.yaml
··· 26 26 '@foxui/social': 27 27 specifier: ^0.8.4 28 28 version: 0.8.4(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))(@tiptap/extensions@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))(highlight.js@11.11.1)(svelte@5.55.0)(tailwindcss@4.2.2) 29 + '@foxui/text': 30 + specifier: ^0.8.2 31 + version: 0.8.2(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))(@tiptap/extensions@3.20.5(@tiptap/core@3.20.5(@tiptap/pm@3.20.5))(@tiptap/pm@3.20.5))(highlight.js@11.11.1)(svelte@5.55.0)(tailwindcss@4.2.2) 29 32 '@foxui/time': 30 33 specifier: ^0.8.2 31 34 version: 0.8.2(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(tailwindcss@4.2.2)
+265 -21
src/lib/components/EventEditor.svelte
··· 6 6 import { notifyContrailOfUpdate } from '$lib/contrail'; 7 7 import { compressImage } from '$lib/atproto/image-helper'; 8 8 import { validateLink } from '$lib/cal/helper'; 9 + import * as TID from '@atcute/tid'; 9 10 import { 10 11 Avatar as FoxAvatar, 11 12 Button, ··· 14 15 PopoverContent, 15 16 ToggleGroup, 16 17 ToggleGroupItem, 17 - Input 18 + Input, 19 + Checkbox 18 20 } from '@foxui/core'; 19 21 import { goto } from '$app/navigation'; 20 22 import { tokenize, type Token } from '@atcute/bluesky-richtext-parser'; ··· 23 25 import { browser } from '$app/environment'; 24 26 import { putImage, getImage, deleteImage } from '$lib/components/image-store'; 25 27 import { Modal } from '@foxui/core'; 28 + import { PlainTextEditor } from '@foxui/text'; 26 29 import Avatar from 'svelte-boring-avatars'; 27 30 import DateTimePicker from '$lib/components/DateTimePicker.svelte'; 28 31 import type { FlatEventRecord } from '$lib/contrail'; ··· 65 68 let thumbnailKey: string | null = $state(null); 66 69 let thumbnailChanged = $state(false); 67 70 68 - let name = $state(''); 71 + // svelte-ignore state_referenced_locally 72 + let name = $state(eventData?.name || ''); 69 73 let description = $state(''); 70 74 let startsAt = $state(''); 71 75 let endsAt = $state(''); ··· 74 78 let thumbnailPreview: string | null = $state(null); 75 79 let submitting = $state(false); 76 80 let error: string | null = $state(null); 77 - let titleEl: HTMLTextAreaElement | undefined = $state(undefined); 81 + import type { Readable } from 'svelte/store'; 82 + import { get } from 'svelte/store'; 83 + import type { Editor } from '@tiptap/core'; 84 + let titleEditor: Readable<Editor> | undefined = $state(undefined); 78 85 79 86 let location: EventLocation | null = $state(null); 80 87 let locationChanged = $state(false); ··· 93 100 94 101 let draftLoaded = $state(false); 95 102 103 + let showRecurringModal = $state(false); 104 + let recurringInterval = $state(1); 105 + let recurringUnit: 'days' | 'weeks' | 'months' | 'years' = $state('weeks'); 106 + let recurringCount = $state(4); 107 + let recurringNumberInTitle = $state(false); 108 + let recurringCreating = $state(false); 109 + let recurringError: string | null = $state(null); 110 + let recurringCreated = $state(0); 111 + 112 + let titleNumberMatch = $derived(name.match(/#?(\d+)\s*$/)); 113 + let detectedStartNumber = $derived(titleNumberMatch ? parseInt(titleNumberMatch[1]) : null); 114 + 115 + $effect(() => { 116 + if (detectedStartNumber !== null) { 117 + recurringNumberInTitle = true; 118 + } 119 + }); 120 + 96 121 function isoToDatetimeLocal(iso: string): string { 97 122 const date = new Date(iso); 98 123 const pad = (n: number) => n.toString().padStart(2, '0'); ··· 203 228 } 204 229 draftLoaded = true; 205 230 if (!startsAt) editingDates = true; 206 - titleEl?.focus(); 231 + if (titleEditor) get(titleEditor)?.commands.focus(); 207 232 }); 208 233 209 234 let saveDraftTimeout: ReturnType<typeof setTimeout> | undefined; ··· 633 658 showDeleteConfirm = false; 634 659 } 635 660 } 661 + 662 + $inspect(name); 663 + 664 + async function handleCreateRecurring() { 665 + if (!name.trim() || !startsAt || !user.isLoggedIn || !user.did) return; 666 + 667 + recurringCreating = true; 668 + recurringError = null; 669 + recurringCreated = 0; 670 + 671 + try { 672 + const baseStart = new Date(startsAt); 673 + const baseEnd = endsAt ? new Date(endsAt) : null; 674 + const duration = baseEnd ? baseEnd.getTime() - baseStart.getTime() : 0; 675 + const baseName = recurringNumberInTitle && titleNumberMatch 676 + ? name.replace(/#?\d+\s*$/, '').trimEnd() 677 + : name.trim(); 678 + const startNum = detectedStartNumber ?? 1; 679 + const hasHash = titleNumberMatch ? titleNumberMatch[0].includes('#') : false; 680 + 681 + // Build the same record shape as handleSubmit 682 + let media: Array<Record<string, unknown>> | undefined; 683 + const existingMedia = (eventData?.media ?? []) as Array<Record<string, unknown>>; 684 + 685 + if (isNew || thumbnailChanged) { 686 + if (thumbnailFile) { 687 + const compressed = await compressImage(thumbnailFile); 688 + const result = await uploadBlob({ blob: compressed.blob }); 689 + if (result) { 690 + const { aspectRatio: _ar, ...blobRef } = result as Record<string, unknown> & { aspectRatio?: unknown }; 691 + media = [ 692 + ...existingMedia.filter((m) => m.role !== 'thumbnail'), 693 + { 694 + role: 'thumbnail', 695 + content: blobRef, 696 + aspect_ratio: { width: compressed.aspectRatio.width, height: compressed.aspectRatio.height } 697 + } 698 + ]; 699 + } 700 + } else { 701 + const remaining = existingMedia.filter((m) => m.role !== 'thumbnail'); 702 + if (remaining.length > 0) media = remaining; 703 + } 704 + } else if (existingMedia.length > 0) { 705 + media = existingMedia; 706 + } 707 + 708 + const parentUri = `at://${user.did}/community.lexicon.calendar.event/${rkey}`; 709 + 710 + for (let i = 0; i < recurringCount; i++) { 711 + const eventStart = new Date(baseStart); 712 + const offset = (i + 1); 713 + if (recurringUnit === 'days') eventStart.setDate(eventStart.getDate() + offset * recurringInterval); 714 + else if (recurringUnit === 'weeks') eventStart.setDate(eventStart.getDate() + offset * recurringInterval * 7); 715 + else if (recurringUnit === 'months') eventStart.setMonth(eventStart.getMonth() + offset * recurringInterval); 716 + else if (recurringUnit === 'years') eventStart.setFullYear(eventStart.getFullYear() + offset * recurringInterval); 717 + 718 + const eventEnd = duration ? new Date(eventStart.getTime() + duration) : null; 719 + 720 + let eventName = baseName; 721 + if (recurringNumberInTitle) { 722 + const num = startNum + (i + 1); 723 + eventName = hasHash ? `${baseName} #${num}` : `${baseName} ${num}`; 724 + } 725 + 726 + const newRkey = TID.now(); 727 + const record: Record<string, unknown> = { 728 + $type: 'community.lexicon.calendar.event', 729 + createdWith: 'https://atmo.rsvp', 730 + name: eventName, 731 + mode: `community.lexicon.calendar.event#${mode}`, 732 + status: 'community.lexicon.calendar.event#scheduled', 733 + startsAt: eventStart.toISOString(), 734 + createdAt: new Date().toISOString(), 735 + recurringEventOf: parentUri 736 + }; 737 + 738 + const trimmedDescription = description.trim(); 739 + if (trimmedDescription) { 740 + record.description = trimmedDescription; 741 + } 742 + if (eventEnd) { 743 + record.endsAt = eventEnd.toISOString(); 744 + } 745 + if (media) { 746 + record.media = media; 747 + } 748 + if (links.length > 0) { 749 + record.uris = links; 750 + } 751 + if (location) { 752 + record.locations = [{ 753 + $type: 'community.lexicon.location.address', 754 + ...location 755 + }]; 756 + } 757 + 758 + const response = await putRecord({ 759 + collection: 'community.lexicon.calendar.event', 760 + rkey: newRkey, 761 + record 762 + }); 763 + 764 + if (response.ok) { 765 + const eventUri = `at://${user.did}/community.lexicon.calendar.event/${newRkey}`; 766 + await notifyContrailOfUpdate(eventUri); 767 + recurringCreated = i + 1; 768 + } else { 769 + recurringError = `Failed to create event ${i + 1}. Stopping.`; 770 + return; 771 + } 772 + } 773 + 774 + showRecurringModal = false; 775 + } catch (e) { 776 + console.error('Failed to create recurring events:', e); 777 + recurringError = 'Failed to create recurring events. Please try again.'; 778 + } finally { 779 + recurringCreating = false; 780 + } 781 + } 636 782 </script> 637 783 638 784 <div class="px-6 py-12 sm:py-12"> ··· 744 890 > 745 891 {submitting 746 892 ? isNew 747 - ? 'Creating...' 893 + ? 'Publishing...' 748 894 : 'Saving...' 749 895 : isNew 750 - ? 'Create Event' 896 + ? 'Publish Event' 751 897 : 'Save Event'} 752 898 </Button> 753 - 754 - <!-- Right column: event details --> 899 + <!-- Right column: event details --> 755 900 <div class="order-2 min-w-0 md:order-0 md:col-start-2 md:row-span-5 md:row-start-1"> 756 901 <!-- Name --> 757 - <div class="mb-2"> 758 - <textarea 759 - bind:this={titleEl} 760 - bind:value={name} 761 - required 902 + <div class="mb-2 min-h-14"> 903 + <PlainTextEditor 904 + content={name} 905 + bind:editor={titleEditor} 762 906 placeholder="Event name" 763 - rows={2} 764 - class="text-base-900 dark:text-base-50 placeholder:text-base-500 dark:placeholder:text-base-500 w-full resize-none border-0 bg-transparent px-0 text-3xl leading-tight font-bold focus:border-0 focus:ring-0 focus:outline-none sm:text-4xl" 765 - ></textarea> 907 + onupdate={() => { 908 + if (titleEditor) name = get(titleEditor)?.getText() ?? ''; 909 + }} 910 + class="text-base-900 dark:text-base-50 placeholder:text-base-500 dark:placeholder:text-base-500 w-full text-3xl leading-tight font-bold focus:outline-none sm:text-4xl" 911 + /> 766 912 </div> 767 913 768 914 <!-- Mode toggle --> ··· 778 924 } 779 925 } 780 926 class="w-fit" 927 + size="xs" 781 928 > 782 - <ToggleGroupItem size="sm" value="inperson">In Person</ToggleGroupItem> 783 - <ToggleGroupItem size="sm" value="virtual">Virtual</ToggleGroupItem> 784 - <ToggleGroupItem size="sm" value="hybrid">Hybrid</ToggleGroupItem> 929 + <ToggleGroupItem value="inperson">In Person</ToggleGroupItem> 930 + <ToggleGroupItem value="virtual">Virtual</ToggleGroupItem> 931 + <ToggleGroupItem value="hybrid">Hybrid</ToggleGroupItem> 785 932 </ToggleGroup> 786 933 </div> 787 934 ··· 1033 1180 <Button type="submit" disabled={submitting || !name.trim() || !startsAt}> 1034 1181 {submitting 1035 1182 ? isNew 1036 - ? 'Creating...' 1183 + ? 'Publishing...' 1037 1184 : 'Saving...' 1038 1185 : isNew 1039 - ? 'Create Event' 1186 + ? 'Publish Event' 1040 1187 : 'Save Changes'} 1041 1188 </Button> 1189 + {#if !isNew} 1190 + <Button 1191 + type="button" 1192 + variant="secondary" 1193 + disabled={submitting || !name.trim() || !startsAt} 1194 + onclick={() => { 1195 + recurringError = null; 1196 + recurringCreated = 0; 1197 + showRecurringModal = true; 1198 + }} 1199 + > 1200 + Add recurring events 1201 + </Button> 1202 + {/if} 1042 1203 </div> 1043 1204 1044 1205 <!-- Hosted By --> ··· 1281 1442 > 1282 1443 </p> 1283 1444 </Modal> 1445 + 1446 + <Modal bind:open={showRecurringModal}> 1447 + <p class="text-base-900 dark:text-base-50 text-lg font-semibold">Add recurring events</p> 1448 + <p class="text-base-500 dark:text-base-400 mt-1 text-sm"> 1449 + Create multiple copies of this event at regular intervals. 1450 + </p> 1451 + 1452 + <div class="mt-4 space-y-4"> 1453 + <div> 1454 + <!-- svelte-ignore a11y_label_has_associated_control --> 1455 + <label class="text-base-700 dark:text-base-300 mb-1 block text-sm font-medium"> 1456 + Number of events to create 1457 + </label> 1458 + <Input type="number" bind:value={recurringCount} min={1} max={52} class="w-24" /> 1459 + </div> 1460 + 1461 + <div> 1462 + <!-- svelte-ignore a11y_label_has_associated_control --> 1463 + <label class="text-base-700 dark:text-base-300 mb-1 block text-sm font-medium"> 1464 + Repeat every 1465 + </label> 1466 + <div class="flex items-center gap-2"> 1467 + <Input type="number" bind:value={recurringInterval} min={1} max={99} class="w-20" /> 1468 + <ToggleGroup type="single" bind:value={recurringUnit}> 1469 + <ToggleGroupItem value="days">days</ToggleGroupItem> 1470 + <ToggleGroupItem value="weeks">weeks</ToggleGroupItem> 1471 + <ToggleGroupItem value="months">months</ToggleGroupItem> 1472 + <ToggleGroupItem value="years">years</ToggleGroupItem> 1473 + </ToggleGroup> 1474 + </div> 1475 + </div> 1476 + 1477 + <div> 1478 + <div class="flex items-center gap-2"> 1479 + <Checkbox bind:checked={recurringNumberInTitle} sizeVariant="sm" /> 1480 + <span class="text-base-700 dark:text-base-300 text-sm font-medium">Number in title</span> 1481 + </div> 1482 + <p class="text-base-500 dark:text-base-400 mt-1 text-xs"> 1483 + {#if recurringNumberInTitle && detectedStartNumber !== null} 1484 + Titles will count up from #{detectedStartNumber + 1} 1485 + {:else if recurringNumberInTitle} 1486 + A number will be appended to each title 1487 + {:else} 1488 + Append a number to each event title 1489 + {/if} 1490 + </p> 1491 + </div> 1492 + </div> 1493 + 1494 + {#if recurringError} 1495 + <p class="mt-4 text-sm text-red-600 dark:text-red-400">{recurringError}</p> 1496 + {/if} 1497 + 1498 + {#if recurringCreating && recurringCreated > 0} 1499 + <p class="text-base-500 dark:text-base-400 mt-4 text-sm"> 1500 + Created {recurringCreated} of {recurringCount} events... 1501 + </p> 1502 + {/if} 1503 + 1504 + {#if recurringCreated > 0 && !recurringCreating} 1505 + <p class="mt-4 text-sm text-green-600 dark:text-green-400"> 1506 + Successfully created {recurringCreated} recurring events! 1507 + </p> 1508 + {/if} 1509 + 1510 + <div class="mt-4 flex justify-end gap-2"> 1511 + <Button 1512 + variant="secondary" 1513 + onclick={() => (showRecurringModal = false)} 1514 + disabled={recurringCreating} 1515 + > 1516 + {recurringCreated > 0 && !recurringCreating ? 'Close' : 'Cancel'} 1517 + </Button> 1518 + {#if !recurringCreated || recurringCreating} 1519 + <Button 1520 + onclick={handleCreateRecurring} 1521 + disabled={recurringCreating || recurringCount < 1} 1522 + > 1523 + {recurringCreating ? `Creating...` : `Create ${recurringCount} event${recurringCount === 1 ? '' : 's'}`} 1524 + </Button> 1525 + {/if} 1526 + </div> 1527 + </Modal>
+85
src/lib/components/EventList.svelte
··· 1 + <script lang="ts"> 2 + import type { FlatEventRecord } from '$lib/contrail'; 3 + import { loadMoreEvents } from '$lib/contrail/events.remote'; 4 + import EventCard from '$lib/components/EventCard.svelte'; 5 + 6 + let { 7 + events, 8 + cursor, 9 + handles = {}, 10 + actor = undefined, 11 + fetchParams, 12 + gridClass = 'grid gap-6 sm:grid-cols-2' 13 + }: { 14 + events: FlatEventRecord[]; 15 + cursor: string | null; 16 + handles?: Record<string, string>; 17 + actor?: string | undefined; 18 + fetchParams: Record<string, string>; 19 + gridClass?: string; 20 + } = $props(); 21 + 22 + let extraEvents = $state<FlatEventRecord[]>([]); 23 + let currentCursor = $state<string | null>(null); 24 + let currentHandles = $state<Record<string, string>>({}); 25 + let loading = $state(false); 26 + 27 + $effect(() => { 28 + currentCursor = cursor; 29 + extraEvents = []; 30 + currentHandles = { ...handles }; 31 + }); 32 + 33 + let allEvents = $derived([...events, ...extraEvents]); 34 + 35 + async function loadMore() { 36 + if (!currentCursor || loading) return; 37 + 38 + loading = true; 39 + 40 + try { 41 + const params: Record<string, unknown> = {}; 42 + for (const [key, value] of Object.entries(fetchParams)) { 43 + if (key === 'limit' || key === 'rsvpsGoingCountMin') { 44 + params[key] = Number(value); 45 + } else if (key === 'profiles') { 46 + params[key] = value === 'true'; 47 + } else { 48 + params[key] = value; 49 + } 50 + } 51 + params.cursor = currentCursor; 52 + 53 + const result = await loadMoreEvents(params as Parameters<typeof loadMoreEvents>[0]); 54 + 55 + extraEvents = [...extraEvents, ...result.events]; 56 + currentCursor = result.cursor; 57 + 58 + if (result.handles) { 59 + currentHandles = { ...currentHandles, ...result.handles }; 60 + } 61 + } catch (err) { 62 + console.error('Failed to load more events:', err); 63 + } finally { 64 + loading = false; 65 + } 66 + } 67 + </script> 68 + 69 + <div class={gridClass}> 70 + {#each allEvents as event (event.uri)} 71 + <EventCard {event} actor={actor ?? currentHandles[event.did]} /> 72 + {/each} 73 + </div> 74 + 75 + {#if currentCursor} 76 + <div class="mt-8 text-center"> 77 + <button 78 + onclick={loadMore} 79 + disabled={loading} 80 + class="bg-base-200 dark:bg-base-800 text-base-900 dark:text-base-50 hover:bg-base-300 dark:hover:bg-base-700 inline-block rounded-xl px-5 py-2 text-sm font-medium transition-colors" 81 + > 82 + {loading ? 'Loading...' : 'Load more'} 83 + </button> 84 + </div> 85 + {/if}
+48
src/lib/contrail/events.remote.ts
··· 1 + import { command, getRequestEvent } from '$app/server'; 2 + import * as v from 'valibot'; 3 + import { getServerClient } from './index'; 4 + import { flattenEventRecords, listEventRecordsFromContrail } from '$lib/contrail'; 5 + import type { ActorIdentifier } from '@atcute/lexicons'; 6 + 7 + const listEventsInput = v.object({ 8 + actor: v.optional(v.string()), 9 + search: v.optional(v.string()), 10 + startsAtMin: v.optional(v.string()), 11 + startsAtMax: v.optional(v.string()), 12 + endsAtMin: v.optional(v.string()), 13 + endsAtMax: v.optional(v.string()), 14 + rsvpsGoingCountMin: v.optional(v.number()), 15 + profiles: v.optional(v.boolean()), 16 + sort: v.optional(v.string()), 17 + order: v.optional(v.picklist(['asc', 'desc'])), 18 + limit: v.optional(v.number()), 19 + cursor: v.optional(v.string()) 20 + }); 21 + 22 + export const loadMoreEvents = command(listEventsInput, async (input) => { 23 + const { platform } = getRequestEvent(); 24 + 25 + const client = getServerClient(platform!.env.DB); 26 + 27 + const response = await listEventRecordsFromContrail(client, { 28 + ...input, 29 + actor: input.actor as ActorIdentifier | undefined 30 + }); 31 + 32 + if (!response) { 33 + return { events: [] as ReturnType<typeof flattenEventRecords>, handles: {} as Record<string, string>, cursor: null as string | null }; 34 + } 35 + 36 + const events = flattenEventRecords(response.records ?? []); 37 + 38 + const handles: Record<string, string> = {}; 39 + for (const p of response.profiles ?? []) { 40 + if (p.handle) handles[p.did] = p.handle; 41 + } 42 + 43 + return { 44 + events, 45 + handles, 46 + cursor: response.cursor ?? null 47 + }; 48 + });
+15 -13
src/routes/(app)/+layout.svelte
··· 47 47 <span class="sr-only">search for events</span> 48 48 </a> 49 49 {#if user.isLoggedIn} 50 - <Button href="/create" class="hidden sm:inline-flex">Create Event</Button> 51 - <Button href="/create" size="icon" class="sm:hidden"> 52 - <svg 53 - xmlns="http://www.w3.org/2000/svg" 54 - fill="none" 55 - viewBox="0 0 24 24" 56 - stroke-width="2" 57 - stroke="currentColor" 58 - class="size-5" 59 - > 60 - <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> 61 - </svg> 62 - </Button> 50 + {#if !page.url.pathname.startsWith('/create')} 51 + <Button href="/create" class="hidden sm:inline-flex">Create Event</Button> 52 + <Button href="/create" size="icon" class="sm:hidden"> 53 + <svg 54 + xmlns="http://www.w3.org/2000/svg" 55 + fill="none" 56 + viewBox="0 0 24 24" 57 + stroke-width="2" 58 + stroke="currentColor" 59 + class="size-5" 60 + > 61 + <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> 62 + </svg> 63 + </Button> 64 + {/if} 63 65 <a href="/p/{user.profile?.handle || user.did}" class="shrink-0"> 64 66 <Avatar 65 67 src={user.profile?.avatar}
+10 -17
src/routes/(app)/events/+page.svelte
··· 1 1 <script lang="ts"> 2 - import EventCard from '$lib/components/EventCard.svelte'; 2 + import EventList from '$lib/components/EventList.svelte'; 3 3 4 4 let { data } = $props(); 5 + 6 + const fetchParams = { 7 + startsAtMin: new Date().toISOString(), 8 + profiles: 'true', 9 + sort: 'startsAt', 10 + order: 'asc', 11 + limit: '20' 12 + }; 5 13 </script> 6 14 7 15 <svelte:head> ··· 14 22 {#if data.events.length === 0} 15 23 <p class="text-base-500 text-center text-lg">No upcoming events found.</p> 16 24 {:else} 17 - <div class="grid gap-6 sm:grid-cols-2"> 18 - {#each data.events as event (event.uri)} 19 - <EventCard {event} actor={data.handles[event.did]} /> 20 - {/each} 21 - </div> 22 - 23 - {#if data.cursor} 24 - <div class="mt-8 text-center"> 25 - <a 26 - href="?cursor={data.cursor}" 27 - class="bg-base-200 dark:bg-base-800 text-base-900 dark:text-base-50 hover:bg-base-300 dark:hover:bg-base-700 inline-block rounded-xl px-5 py-2 text-sm font-medium transition-colors" 28 - > 29 - Load more 30 - </a> 31 - </div> 32 - {/if} 25 + <EventList events={data.events} cursor={data.cursor} handles={data.handles} {fetchParams} /> 33 26 {/if} 34 27 </div>
+17 -17
src/routes/(app)/p/[actor]/hosting/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { getProfileBlobUrl } from '$lib/contrail'; 3 - import EventCard from '$lib/components/EventCard.svelte'; 3 + import EventList from '$lib/components/EventList.svelte'; 4 4 5 5 let { data } = $props(); 6 6 ··· 10 10 let hostAvatar = $derived( 11 11 hostProfile?.record?.avatar ? getProfileBlobUrl(hostDid, hostProfile.record.avatar) : undefined 12 12 ); 13 + 14 + let fetchParams: Record<string, string> = $derived({ 15 + profiles: 'true', 16 + sort: 'startsAt', 17 + order: 'asc', 18 + startsAtMin: new Date().toISOString(), 19 + ...(data.actor ? { actor: data.actor } : {}), 20 + limit: '20' 21 + }); 13 22 </script> 14 23 15 24 <svelte:head> ··· 39 48 </a> 40 49 41 50 {#if (data.events?.length ?? 0) > 0} 42 - <div class="space-y-3"> 43 - {#each data.events as event (event.uri)} 44 - <EventCard {event} actor={data.actor} /> 45 - {/each} 46 - </div> 47 - 48 - {#if data.cursor} 49 - <div class="mt-6 text-center"> 50 - <a 51 - href="?cursor={data.cursor}" 52 - class="bg-base-200 dark:bg-base-800 text-base-900 dark:text-base-50 hover:bg-base-300 dark:hover:bg-base-700 inline-block rounded-xl px-5 py-2 text-sm font-medium transition-colors" 53 - > 54 - Load more 55 - </a> 56 - </div> 57 - {/if} 51 + <EventList 52 + events={data.events ?? []} 53 + cursor={data.cursor ?? null} 54 + actor={data.actor} 55 + {fetchParams} 56 + gridClass="space-y-3" 57 + /> 58 58 {:else} 59 59 <p class="text-base-500 dark:text-base-400 py-12 text-center"> 60 60 No upcoming events found.
+17 -17
src/routes/(app)/p/[actor]/past-events/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { getProfileBlobUrl } from '$lib/contrail'; 3 - import EventCard from '$lib/components/EventCard.svelte'; 3 + import EventList from '$lib/components/EventList.svelte'; 4 4 5 5 let { data } = $props(); 6 6 ··· 10 10 let hostAvatar = $derived( 11 11 hostProfile?.record?.avatar ? getProfileBlobUrl(hostDid, hostProfile.record.avatar) : undefined 12 12 ); 13 + 14 + let fetchParams: Record<string, string> = $derived({ 15 + profiles: 'true', 16 + sort: 'startsAt', 17 + order: 'desc', 18 + startsAtMax: new Date().toISOString(), 19 + ...(data.actor ? { actor: data.actor } : {}), 20 + limit: '20' 21 + }); 13 22 </script> 14 23 15 24 <svelte:head> ··· 39 48 </a> 40 49 41 50 {#if (data.events?.length ?? 0) > 0} 42 - <div class="space-y-3"> 43 - {#each data.events as event (event.uri)} 44 - <EventCard {event} actor={data.actor} /> 45 - {/each} 46 - </div> 47 - 48 - {#if data.cursor} 49 - <div class="mt-6 text-center"> 50 - <a 51 - href="?cursor={data.cursor}" 52 - class="bg-base-200 dark:bg-base-800 text-base-900 dark:text-base-50 hover:bg-base-300 dark:hover:bg-base-700 inline-block rounded-xl px-5 py-2 text-sm font-medium transition-colors" 53 - > 54 - Load more 55 - </a> 56 - </div> 57 - {/if} 51 + <EventList 52 + events={data.events ?? []} 53 + cursor={data.cursor ?? null} 54 + actor={data.actor} 55 + {fetchParams} 56 + gridClass="space-y-3" 57 + /> 58 58 {:else} 59 59 <p class="text-base-500 dark:text-base-400 py-12 text-center"> 60 60 No past events found.
+13 -17
src/routes/(app)/search/+page.svelte
··· 1 1 <script lang="ts"> 2 - import EventCard from '$lib/components/EventCard.svelte'; 2 + import EventList from '$lib/components/EventList.svelte'; 3 3 import { Input, Button } from '@foxui/core'; 4 4 import { goto } from '$app/navigation'; 5 5 ··· 37 37 {#if data.events.length === 0} 38 38 <p class="text-base-500 py-8 text-center">No events found for "{data.query}".</p> 39 39 {:else} 40 - <div class="grid gap-6 sm:grid-cols-2"> 41 - {#each data.events as event (event.uri)} 42 - <EventCard {event} actor={data.handles[event.did]} /> 43 - {/each} 44 - </div> 45 - 46 - {#if data.cursor} 47 - <div class="mt-8 text-center"> 48 - <a 49 - href="?q={encodeURIComponent(data.query)}&cursor={data.cursor}" 50 - class="bg-base-200 dark:bg-base-800 text-base-900 dark:text-base-50 hover:bg-base-300 dark:hover:bg-base-700 inline-block rounded-xl px-5 py-2 text-sm font-medium transition-colors" 51 - > 52 - Load more 53 - </a> 54 - </div> 55 - {/if} 40 + <EventList 41 + events={data.events} 42 + cursor={data.cursor} 43 + handles={data.handles} 44 + fetchParams={{ 45 + search: data.query, 46 + profiles: 'true', 47 + sort: 'startsAt', 48 + order: 'desc', 49 + limit: '20' 50 + }} 51 + /> 56 52 {/if} 57 53 {/if} 58 54 </div>