Flesh out calendar event cards

This commit is contained in:
Jon Staab
2024-09-20 11:00:41 -07:00
parent 0d53934152
commit 88000d02ba
14 changed files with 153 additions and 69 deletions

49
package-lock.json generated
View File

@@ -23,12 +23,12 @@
"@tiptap/extension-text": "^2.6.6",
"@tiptap/suggestion": "^2.6.4",
"@types/throttle-debounce": "^5.0.2",
"@welshman/app": "^0.0.5",
"@welshman/app": "^0.0.7",
"@welshman/lib": "^0.0.17",
"@welshman/net": "^0.0.22",
"@welshman/signer": "^0.0.5",
"@welshman/store": "^0.0.6",
"@welshman/util": "^0.0.31",
"@welshman/store": "^0.0.7",
"@welshman/util": "^0.0.33",
"daisyui": "^4.12.10",
"date-picker-svelte": "^2.13.0",
"fuse.js": "^7.0.0",
@@ -1655,14 +1655,14 @@
}
},
"node_modules/@welshman/app": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@welshman/app/-/app-0.0.5.tgz",
"integrity": "sha512-T7iXybwBnFN9E1GWeGQ6ZxvFrQ3EUUWHsZdG3Q+s57YxBDqDkRy+eOtxaDb2EBmgyflheERaggOlzFFTsmUjyA==",
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@welshman/app/-/app-0.0.7.tgz",
"integrity": "sha512-37dTcUDe+6wLhCFClexuJZKCZyGHgGoiFBvm8EL6ig4UHRMRWH2NCeddftScq0gEIl2GVWuq96iCni9COc7nRw==",
"dependencies": {
"@welshman/lib": "0.0.17",
"@welshman/net": "0.0.22",
"@welshman/signer": "0.0.5",
"@welshman/store": "0.0.6",
"@welshman/store": "0.0.7",
"@welshman/util": "0.0.31",
"fuse.js": "^7.0.0",
"idb": "^8.0.0",
@@ -1670,6 +1670,15 @@
"throttle-debounce": "^5.0.2"
}
},
"node_modules/@welshman/app/node_modules/@welshman/util": {
"version": "0.0.31",
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.31.tgz",
"integrity": "sha512-nUv/Mto6maQx6vbCaZ0HKQRvLYRWqQzPTLsKoxSDX0HkeFP653T6raFx8TvWt+op/qBuuT8sANJactc2/qGoYg==",
"dependencies": {
"@welshman/lib": "0.0.17",
"nostr-tools": "^2.7.2"
}
},
"node_modules/@welshman/lib": {
"version": "0.0.17",
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.17.tgz",
@@ -1693,6 +1702,15 @@
"ws": "^8.16.0"
}
},
"node_modules/@welshman/net/node_modules/@welshman/util": {
"version": "0.0.31",
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.31.tgz",
"integrity": "sha512-nUv/Mto6maQx6vbCaZ0HKQRvLYRWqQzPTLsKoxSDX0HkeFP653T6raFx8TvWt+op/qBuuT8sANJactc2/qGoYg==",
"dependencies": {
"@welshman/lib": "0.0.17",
"nostr-tools": "^2.7.2"
}
},
"node_modules/@welshman/signer": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@welshman/signer/-/signer-0.0.5.tgz",
@@ -1725,16 +1743,16 @@
}
},
"node_modules/@welshman/store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@welshman/store/-/store-0.0.6.tgz",
"integrity": "sha512-yZDnXcIZ2fcqvK+1W9OTRXJy3IPKE3ykFp5VnUT5bGMSRSTIyHnPO5swsgDLofe5Zswdee0lJTwE5IQBthf0pQ==",
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@welshman/store/-/store-0.0.7.tgz",
"integrity": "sha512-Ltx2RYwMicCeGWBdl3+Wr+pilIdofmNSDKTqgOqtPanOgSarGn7uqZnPzDz9N+P0htg2TDSogg7YkBWf5drhDw==",
"dependencies": {
"@welshman/lib": "0.0.17",
"@welshman/util": "0.0.31",
"svelte": "^4.2.18"
}
},
"node_modules/@welshman/util": {
"node_modules/@welshman/store/node_modules/@welshman/util": {
"version": "0.0.31",
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.31.tgz",
"integrity": "sha512-nUv/Mto6maQx6vbCaZ0HKQRvLYRWqQzPTLsKoxSDX0HkeFP653T6raFx8TvWt+op/qBuuT8sANJactc2/qGoYg==",
@@ -1743,6 +1761,15 @@
"nostr-tools": "^2.7.2"
}
},
"node_modules/@welshman/util": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.33.tgz",
"integrity": "sha512-dUt6H6Bi3cwhLup7G6MsfZymwX4C/EGvm6X/9KvaoTGZhy9z66lpoa1MebLGOTaQDyMs8nrIckXMp9eAi/c15Q==",
"dependencies": {
"@welshman/lib": "0.0.17",
"nostr-tools": "^2.7.2"
}
},
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",

View File

@@ -48,12 +48,12 @@
"@tiptap/extension-text": "^2.6.6",
"@tiptap/suggestion": "^2.6.4",
"@types/throttle-debounce": "^5.0.2",
"@welshman/app": "^0.0.5",
"@welshman/lib": "^0.0.17",
"@welshman/util": "^0.0.33",
"@welshman/store": "^0.0.7",
"@welshman/net": "^0.0.22",
"@welshman/signer": "^0.0.5",
"@welshman/store": "^0.0.6",
"@welshman/util": "^0.0.31",
"@welshman/app": "^0.0.7",
"daisyui": "^4.12.10",
"date-picker-svelte": "^2.13.0",
"fuse.js": "^7.0.0",

View File

@@ -43,7 +43,7 @@
}
.card2 {
@apply rounded-box bg-base-100 p-6 text-base-content;
@apply rounded-box bg-base-100 p-6 text-base-content overflow-hidden text-ellipsis;
}
.card2.card2-sm {
@@ -90,6 +90,10 @@
@apply shadow-[0_20px_25px_-5px_rgb(0,0,0,0.1)_0_8px_10px_-6px_rgb(0,0,0,0.1)];
}
.modal-box .z-feature {
@apply z-modal-feature;
}
/* tiptap */
.input-editor, .chat-editor {

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import cx from "classnames"
import {fromPairs} from "@welshman/lib"
import {Tags, getAddress} from "@welshman/util"
import {repository, pubkey, secondsToDate, getLocale, formatTimestamp, formatTimestampAsDate, deriveProfileDisplay} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
export let event
const address = getAddress(event)
const timeFmt = new Intl.DateTimeFormat(getLocale(), {timeStyle: "short"})
const datetimeFmt = new Intl.DateTimeFormat(getLocale(), {dateStyle: "short", timeStyle: "short"})
const profileDisplay = deriveProfileDisplay(event.pubkey)
$: meta = fromPairs(event.tags) as Record<string, string>
$: end = parseInt(meta.end)
$: start = parseInt(meta.start)
$: startDate = secondsToDate(start)
$: endDate = secondsToDate(end)
$: startDateDisplay = formatTimestampAsDate(start)
$: endDateDisplay = formatTimestampAsDate(end)
$: isSingleDay = startDateDisplay === endDateDisplay
</script>
<div class="card2 flex justify-between items-center gap-2">
<span>{meta.title || meta.name}</span>
<div class="flex items-center gap-2 text-sm">
<Icon icon="clock-circle" size={4} />
{timeFmt.format(startDate)}{isSingleDay ? timeFmt.format(endDate) : formatTimestamp(end)}
</div>
</div>

View File

@@ -4,21 +4,22 @@
import {writable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {NProfileExtension, ImageExtension} from "nostr-editor"
import {randomId} from "@welshman/lib"
import {createEvent, EVENT_DATE, EVENT_TIME} from "@welshman/util"
import {publishThunk, makeThunk} from "@welshman/app"
import {publishThunk, makeThunk, dateToSeconds} from "@welshman/app"
import {findNodes} from "@lib/tiptap"
import Icon from "@lib/components/Icon.svelte"
import Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.svelte"
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
import {makeMention, makeIMeta} from "@app/commands"
import {getNoteEditorOptions, addFile} from "@app/editor"
import {pushModal} from "@app/modal"
import {getNoteEditorOptions, addFile, uploadFiles} from "@app/editor"
import {pushModal, clearModal} from "@app/modal"
import {pushToast} from "@app/toast"
export let url
const next = () => null
const submit = () => uploadFiles($editor)
const back = () => history.back()
@@ -50,12 +51,20 @@
const event = createEvent(kind, {
content: $editor.getText(),
tags: [["-"], ...mentionTags, ...imetaTags],
tags: [
["-"],
["d", randomId()],
["title", title],
["location", location],
["start", dateToSeconds(start).toString()],
["end", dateToSeconds(end).toString()],
...mentionTags,
...imetaTags,
],
})
publishThunk(makeThunk({event, relays: [url]}))
$editor.chain().clearContent().run()
clearModal()
}
let editor: Readable<Editor>
@@ -71,7 +80,7 @@
})
</script>
<form class="column gap-4" on:submit|preventDefault={next}>
<form class="column gap-4" on:submit|preventDefault={submit}>
<div class="py-2">
<h1 class="heading">Create an Event</h1>
<p class="text-center">Invite other group members to events online or in real life.</p>
@@ -118,7 +127,7 @@
<p slot="label">Location (optional)</p>
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
<Icon icon="map-point" />
<input bind:value={title} class="grow" type="text" />
<input bind:value={location} class="grow" type="text" />
</label>
</Field>
<div class="flex flex-row items-center justify-between gap-4">
@@ -127,8 +136,7 @@
Go back
</Button>
<Button type="submit" class="btn btn-primary">
Next
<Icon icon="alt-arrow-right" class="!bg-base-300" />
Create Event
</Button>
</div>
</form>

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="#1C274C" stroke-width="1.5"/>
<path d="M12 8V12L14.5 14.5" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@@ -20,17 +20,21 @@
}
</script>
<label class="relative">
<DateInput format="yyyy-MM-dd HH:mm" placeholder="" bind:value />
<Button class="relative" on:click={init}>
<DateInput
format="yyyy-MM-dd HH:mm"
timePrecision="minute"
placeholder=""
bind:value />
<div class="absolute top-0 h-12 right-2 flex gap-2 items-center cursor-pointer">
{#if value}
<Button on:click={clear} class="h-5">
<Icon icon="close-circle" />
</Button>
{:else}
<Button on:click={init} class="h-5">
<Button class="h-5">
<Icon icon="calendar-minimalistic" />
</Button>
{/if}
</div>
</label>
</Button>

View File

@@ -0,0 +1,5 @@
<div class="flex items-center gap-2 p-2 text-xs opacity-50">
<div class="h-px flex-grow bg-base-content opacity-25" />
<p><slot /></p>
<div class="h-px flex-grow bg-base-content opacity-25" />
</div>

View File

@@ -26,6 +26,7 @@
import ChatRound from "@assets/icons/Chat Round.svg?dataurl"
import CheckCircle from "@assets/icons/Check Circle.svg?dataurl"
import ClipboardText from "@assets/icons/Clipboard Text.svg?dataurl"
import ClockCircle from "@assets/icons/Clock Circle.svg?dataurl"
import CloseCircle from "@assets/icons/Close Circle.svg?dataurl"
import Copy from "@assets/icons/Copy.svg?dataurl"
import Compass from "@assets/icons/Compass.svg?dataurl"
@@ -90,6 +91,7 @@
"chat-round": ChatRound,
"check-circle": CheckCircle,
"clipboard-text": ClipboardText,
"clock-circle": ClockCircle,
"close-circle": CloseCircle,
copy: Copy,
compass: Compass,

View File

@@ -68,7 +68,7 @@ export const createSuggestions = (options: SuggestionsOptions) =>
popover = tippy("body", {
getReferenceClientRect: props.clientRect as any,
appendTo: document.body,
appendTo: document.querySelector('dialog[open]') || document.body,
content: target,
showOnCreate: true,
interactive: true,

View File

@@ -6,6 +6,7 @@
import {goto} from "$app/navigation"
import {browser} from "$app/environment"
import {sleep} from "@welshman/lib"
import {throttled} from "@welshman/store"
import {
relays,
handles,
@@ -22,7 +23,6 @@
tracker,
} from "@welshman/app"
import * as app from "@welshman/app"
import {createEventStore} from "@welshman/store"
import ModalBox from "@lib/components/ModalBox.svelte"
import Toast from "@app/components/Toast.svelte"
import Landing from "@app/components/Landing.svelte"
@@ -64,22 +64,13 @@
ready = db
? Promise.resolve()
: initStorage("flotilla", 3, {
events: {
keyPath: "id",
store: createEventStore(repository),
},
relays: {
keyPath: "url",
store: relays,
},
handles: {
keyPath: "nip05",
store: handles,
},
: initStorage("flotilla", 4, {
events: storageAdapters.fromRepository(repository, {throttle: 300}),
relays: {keyPath: "url", store: throttled(1000, relays)},
handles: {keyPath: "nip05", store: throttled(1000, handles)},
publishStatus: storageAdapters.fromObjectStore(publishStatusData),
freshness: storageAdapters.fromObjectStore(freshness),
plaintext: storageAdapters.fromObjectStore(plaintext),
freshness: storageAdapters.fromObjectStore(freshness, {throttle: 1000}),
plaintext: storageAdapters.fromObjectStore(plaintext, {throttle: 1000}),
tracker: storageAdapters.fromTracker(tracker),
}).then(() => sleep(300)) // Wait an extra few ms because of repository throttle

View File

@@ -15,6 +15,7 @@
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Divider from "@lib/components/Divider.svelte"
import ChatMessage from "@app/components/ChatMessage.svelte"
import ChatCompose from "@app/components/ChatCompose.svelte"
import {userMembership, decodeNRelay, makeChatId, deriveChat} from "@app/state"
@@ -90,11 +91,7 @@
<div class="-mt-2 flex flex-grow flex-col-reverse overflow-auto py-2">
{#each elements as { type, id, value, showPubkey } (id)}
{#if type === "date"}
<div class="flex items-center gap-2 p-2 text-xs opacity-50">
<div class="h-px flex-grow bg-base-content opacity-25" />
<p>{value}</p>
<div class="h-px flex-grow bg-base-content opacity-25" />
</div>
<Divider>{value}</Divider>
{:else}
<ChatMessage event={assertEvent(value)} {showPubkey} />
{/if}

View File

@@ -1,29 +1,39 @@
<script lang="ts">
import {page} from "$app/stores"
import {sortBy, last} from '@welshman/lib'
import type {TrustedEvent} from '@welshman/util'
import {formatTimestampAsDate} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Divider from "@lib/components/Divider.svelte"
import EventCard from "@app/components/EventCard.svelte"
import EventCreate from "@app/components/EventCreate.svelte"
import {pushModal} from "@app/modal"
import {eventsByUrl, decodeNRelay} from "@app/state"
const url = decodeNRelay($page.params.nrelay)
const createEvent = () => pushModal(EventCreate)
const createEvent = () => pushModal(EventCreate, {url})
const getDateDisplay = (event: TrustedEvent, reset: boolean) => {
if (reset) {
prevEvent = undefined
}
const getStart = (event: TrustedEvent) =>
parseInt(event.tags.find(t => t[0] === 'start')?.[1]!)
return "hi"
}
let prevEvent
let loading = true
$: events = $eventsByUrl.get(url) || []
type Item = {
event: TrustedEvent
dateDisplay?: string
}
$: items = sortBy(getStart, $eventsByUrl.get(url) || [])
.reduce<Item[]>((r, event) => {
const prevDateDisplay = r.length > 0 ? formatTimestampAsDate(getStart(last(r).event)) : undefined
const newDateDisplay = formatTimestampAsDate(getStart(event))
const dateDisplay = prevDateDisplay === newDateDisplay ? undefined : newDateDisplay
return [...r, {event, dateDisplay}]
}, [])
setTimeout(() => {
loading = false
@@ -40,19 +50,18 @@
</div>
</div>
</div>
<div class="-mt-2 flex flex-grow flex-col overflow-auto py-2">
{#each events as event, i (event.id)}
{@const dateDisplay = getDateDisplay(event, i === 0)}
<div class="flex flex-grow flex-col overflow-auto p-2 gap-2">
{#each items as {event, dateDisplay}, i (event.id)}
{#if dateDisplay}
<div>{dateDisplay}</div>
<Divider>{dateDisplay}</Divider>
{/if}
<div>{event.id}</div>
<EventCard {event} />
{/each}
<p class="flex h-10 items-center justify-center py-20">
<Spinner {loading}>
{#if loading}
Looking for events...
{:else if events.length === 0}
{:else if items.length === 0}
No events found.
{/if}
</Spinner>

View File

@@ -12,7 +12,8 @@ export default {
feature: 3,
popover: 4,
modal: 5,
toast: 6,
"modal-feature": 6,
toast: 7,
},
},
plugins: [daisyui],