mirror of
https://github.com/coracle-social/flotilla.git
synced 2025-12-10 02:47:06 +00:00
Flesh out calendar event cards
This commit is contained in:
49
package-lock.json
generated
49
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
32
src/app/components/EventCard.svelte
Normal file
32
src/app/components/EventCard.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
4
src/assets/icons/Clock Circle.svg
Normal file
4
src/assets/icons/Clock Circle.svg
Normal 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 |
@@ -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>
|
||||
|
||||
5
src/lib/components/Divider.svelte
Normal file
5
src/lib/components/Divider.svelte
Normal 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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -12,7 +12,8 @@ export default {
|
||||
feature: 3,
|
||||
popover: 4,
|
||||
modal: 5,
|
||||
toast: 6,
|
||||
"modal-feature": 6,
|
||||
toast: 7,
|
||||
},
|
||||
},
|
||||
plugins: [daisyui],
|
||||
|
||||
Reference in New Issue
Block a user