Use address for page param for replaceable events
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {getTagValue} from "@welshman/util"
|
import {getTagValue} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
|
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
@@ -14,7 +15,6 @@
|
|||||||
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
import {makeCalendarPath, makeSpacePath} from "@app/util/routes"
|
import {makeCalendarPath, makeSpacePath} from "@app/util/routes"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
const {url, event, showRoom, showActivity}: Props = $props()
|
const {url, event, showRoom, showActivity}: Props = $props()
|
||||||
|
|
||||||
const h = getTagValue("h", event.tags)
|
const h = getTagValue("h", event.tags)
|
||||||
const path = makeCalendarPath(url, event.id)
|
const path = makeCalendarPath(url, getAddress(event))
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
const editEvent = () => pushModal(CalendarEventEdit, {url, event})
|
const editEvent = () => pushModal(CalendarEventEdit, {url, event})
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<Link
|
<Link
|
||||||
class="col-3 card2 bg-alt w-full cursor-pointer shadow-md"
|
class="col-3 card2 bg-alt w-full cursor-pointer shadow-md"
|
||||||
href={makeCalendarPath(url, event.id)}>
|
href={makeCalendarPath(url, getAddress(event))}>
|
||||||
<CalendarEventHeader {event} />
|
<CalendarEventHeader {event} />
|
||||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {getTagValue} from "@welshman/util"
|
import {getTagValue} from "@welshman/util"
|
||||||
|
import {pubkey} from "@welshman/app"
|
||||||
|
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
import RoomName from "@app/components/RoomName.svelte"
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||||
import EventActivity from "@app/components/EventActivity.svelte"
|
import EventActivity from "@app/components/EventActivity.svelte"
|
||||||
import EventActions from "@app/components/EventActions.svelte"
|
import EventActions from "@app/components/EventActions.svelte"
|
||||||
|
import ClassifiedEdit from "@app/components/ClassifiedEdit.svelte"
|
||||||
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
import {makeClassifiedPath, makeSpacePath} from "@app/util/routes"
|
import {makeClassifiedPath, makeSpacePath} from "@app/util/routes"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: string
|
url: string
|
||||||
@@ -20,9 +26,11 @@
|
|||||||
const {url, event, showRoom, showActivity}: Props = $props()
|
const {url, event, showRoom, showActivity}: Props = $props()
|
||||||
|
|
||||||
const h = getTagValue("h", event.tags)
|
const h = getTagValue("h", event.tags)
|
||||||
const path = makeClassifiedPath(url, event.id)
|
const path = makeClassifiedPath(url, getAddress(event))
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
|
const editClassified = () => pushModal(ClassifiedEdit, {url, event})
|
||||||
|
|
||||||
const deleteReaction = async (event: TrustedEvent) =>
|
const deleteReaction = async (event: TrustedEvent) =>
|
||||||
publishDelete({relays: [url], event, protect: await shouldProtect})
|
publishDelete({relays: [url], event, protect: await shouldProtect})
|
||||||
|
|
||||||
@@ -41,5 +49,16 @@
|
|||||||
{#if showActivity}
|
{#if showActivity}
|
||||||
<EventActivity {url} {path} {event} />
|
<EventActivity {url} {path} {event} />
|
||||||
{/if}
|
{/if}
|
||||||
<EventActions {url} {event} noun="Listing" />
|
<EventActions {url} {event} noun="Listing">
|
||||||
|
{#snippet customActions()}
|
||||||
|
{#if event.pubkey === $pubkey}
|
||||||
|
<li>
|
||||||
|
<Button onclick={editClassified}>
|
||||||
|
<Icon size={4} icon={Pen2} />
|
||||||
|
Edit Listing
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</EventActions>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,25 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {makeEvent, CLASSIFIED} from "@welshman/util"
|
|
||||||
import {publishThunk} from "@welshman/app"
|
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Field from "@lib/components/Field.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ClassifiedForm from "@app/components/ClassifiedForm.svelte"
|
||||||
import Modal from "@lib/components/Modal.svelte"
|
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
|
||||||
import ImagesInput from "@lib/components/ImagesInput.svelte"
|
|
||||||
import CurrencyInput from "@app/components/CurrencyInput.svelte"
|
|
||||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
|
||||||
import {pushToast} from "@app/util/toast"
|
|
||||||
import {PROTECTED} from "@app/core/state"
|
|
||||||
import {makeEditor} from "@app/editor"
|
|
||||||
import {canEnforceNip70, uploadFile} from "@app/core/commands"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@@ -27,148 +10,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {url, h}: Props = $props()
|
const {url, h}: Props = $props()
|
||||||
|
|
||||||
const shouldProtect = canEnforceNip70(url)
|
|
||||||
|
|
||||||
const back = () => history.back()
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
loading = true
|
|
||||||
|
|
||||||
if (!title) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: "Please provide a title for your listing.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const ed = await editor
|
|
||||||
const content = ed.getText({blockSeparator: "\n"}).trim()
|
|
||||||
|
|
||||||
if (!content.trim()) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: "Please provide a description for your listing.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = [...ed.storage.nostr.getEditorTags(), ["summary", content], ["title", title]]
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (await shouldProtect) {
|
|
||||||
tags.push(PROTECTED)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (h) {
|
|
||||||
tags.push(["h", h])
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const image of images) {
|
|
||||||
if (typeof image === "string") {
|
|
||||||
tags.push(["image", image])
|
|
||||||
} else {
|
|
||||||
const {result, error} = await uploadFile(image, {url})
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: `Failed to upload file ${image.name}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
tags.push(["image", result.url])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
publishThunk({
|
|
||||||
relays: [url],
|
|
||||||
event: makeEvent(CLASSIFIED, {content, tags}),
|
|
||||||
})
|
|
||||||
|
|
||||||
history.back()
|
|
||||||
} finally {
|
|
||||||
loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const editor = makeEditor({
|
|
||||||
url,
|
|
||||||
submit,
|
|
||||||
placeholder: "Provide a detailed description for your listing.",
|
|
||||||
})
|
|
||||||
|
|
||||||
let title = $state("")
|
|
||||||
let loading = $state(false)
|
|
||||||
let currencyCode = $state("SAT")
|
|
||||||
let currencyAmount = $state(0)
|
|
||||||
let images = $state<(string | File)[]>([])
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
<ClassifiedForm {url} {h}>
|
||||||
<ModalBody>
|
{#snippet header()}
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<ModalTitle>Create a Classified Listing</ModalTitle>
|
<ModalTitle>Create a Classified Listing</ModalTitle>
|
||||||
<ModalSubtitle>Advertise a job, sale, or need.</ModalSubtitle>
|
<ModalSubtitle>Advertise a job, sale, or need.</ModalSubtitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<div class="col-8 relative">
|
{/snippet}
|
||||||
<Field>
|
</ClassifiedForm>
|
||||||
{#snippet label()}
|
|
||||||
<p>Title*</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<!-- svelte-ignore a11y_autofocus -->
|
|
||||||
<input
|
|
||||||
autofocus={!isMobile}
|
|
||||||
bind:value={title}
|
|
||||||
class="grow"
|
|
||||||
type="text"
|
|
||||||
placeholder="What is this listing for?" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Description*</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<div class="note-editor flex-grow overflow-hidden">
|
|
||||||
<EditorContent {editor} />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Price*</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<div class="join grid grid-cols-2">
|
|
||||||
<label class="join-item input input-bordered flex w-full items-center gap-2">
|
|
||||||
<input bind:value={currencyAmount} class="grow w-32" type="number" />
|
|
||||||
</label>
|
|
||||||
<CurrencyInput class="join-item" bind:value={currencyCode} />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Images (optional)</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<ImagesInput bind:value={images} />
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
|
||||||
<Icon icon={AltArrowLeft} />
|
|
||||||
Go back
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
|
||||||
<Spinner {loading}>Create Listing</Spinner>
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {fromPairs} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {getTag, getTagValues} from "@welshman/util"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import ClassifiedForm from "@app/components/ClassifiedForm.svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
|
const {content} = event
|
||||||
|
const {d, title, status} = fromPairs(event.tags)
|
||||||
|
const [_, price = 0, currency = "SAT"] = getTag("price", event.tags) || []
|
||||||
|
const images = getTagValues("image", event.tags)
|
||||||
|
const initialValues = {d, title, status, content, price, currency, images}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ClassifiedForm {url} {initialValues}>
|
||||||
|
{#snippet header()}
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Edit this Listing</ModalTitle>
|
||||||
|
<ModalSubtitle>Advertise a job, sale, or need.</ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
{/snippet}
|
||||||
|
</ClassifiedForm>
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {Snippet} from "svelte"
|
||||||
|
import {randomId} from "@welshman/lib"
|
||||||
|
import {makeEvent, CLASSIFIED} from "@welshman/util"
|
||||||
|
import {publishThunk} from "@welshman/app"
|
||||||
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ImagesInput from "@lib/components/ImagesInput.svelte"
|
||||||
|
import CurrencyInput from "@app/components/CurrencyInput.svelte"
|
||||||
|
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {PROTECTED} from "@app/core/state"
|
||||||
|
import {makeEditor} from "@app/editor"
|
||||||
|
import {canEnforceNip70, uploadFile} from "@app/core/commands"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
h?: string
|
||||||
|
header: Snippet
|
||||||
|
initialValues?: {
|
||||||
|
d?: string
|
||||||
|
title?: string
|
||||||
|
content?: string
|
||||||
|
price?: number
|
||||||
|
currency?: string
|
||||||
|
images?: string[]
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h, header, initialValues}: Props = $props()
|
||||||
|
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!title) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please provide a title for your listing.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ed = await editor
|
||||||
|
const content = ed.getText({blockSeparator: "\n"}).trim()
|
||||||
|
|
||||||
|
if (!content.trim()) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please provide a description for your listing.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = [
|
||||||
|
["d", initialValues?.d || randomId()],
|
||||||
|
["title", title],
|
||||||
|
["summary", content],
|
||||||
|
["price", String(price), currency],
|
||||||
|
["status", status],
|
||||||
|
...ed.storage.nostr.getEditorTags(),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (await shouldProtect) {
|
||||||
|
tags.push(PROTECTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (h) {
|
||||||
|
tags.push(["h", h])
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const image of images) {
|
||||||
|
if (typeof image === "string") {
|
||||||
|
tags.push(["image", image])
|
||||||
|
} else {
|
||||||
|
const {result, error} = await uploadFile(image, {url})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: `Failed to upload file ${image.name}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
tags.push(["image", result.url])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
publishThunk({
|
||||||
|
relays: [url],
|
||||||
|
event: makeEvent(CLASSIFIED, {content, tags}),
|
||||||
|
})
|
||||||
|
|
||||||
|
history.back()
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = initialValues?.content || ""
|
||||||
|
const editor = makeEditor({url, submit, content})
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
let title = $state(initialValues?.title || "")
|
||||||
|
let status = $state(initialValues?.status || "active")
|
||||||
|
let price = $state(Number(initialValues?.price || 0))
|
||||||
|
let currency = $state(initialValues?.currency || "SAT")
|
||||||
|
let images = $state<(string | File)[]>(initialValues?.images || [])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Create a Classified Listing</ModalTitle>
|
||||||
|
<ModalSubtitle>Advertise a job, sale, or need.</ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<div class="col-8 relative">
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Title*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<!-- svelte-ignore a11y_autofocus -->
|
||||||
|
<input
|
||||||
|
autofocus={!isMobile}
|
||||||
|
bind:value={title}
|
||||||
|
class="grow"
|
||||||
|
type="text"
|
||||||
|
placeholder="What is this listing for?" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Description*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div class="note-editor flex-grow overflow-hidden">
|
||||||
|
<EditorContent {editor} />
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Price*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div class="join grid grid-cols-2">
|
||||||
|
<label class="join-item input input-bordered flex w-full items-center gap-2">
|
||||||
|
<input bind:value={price} class="grow w-32" type="number" />
|
||||||
|
</label>
|
||||||
|
<CurrencyInput class="join-item" bind:value={currency} />
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
{#if initialValues}
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Status*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<select class="select select-bordered w-full" bind:value={status}>
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="sold">Sold</option>
|
||||||
|
</select>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
{/if}
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Images (optional)</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<ImagesInput bind:value={images} />
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
<Spinner {loading}>Create Listing</Spinner>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<Link
|
<Link
|
||||||
class="col-2 card2 bg-alt w-full cursor-pointer shadow-xl"
|
class="col-2 card2 bg-alt w-full cursor-pointer shadow-xl"
|
||||||
href={makeClassifiedPath(url, event.id)}>
|
href={makeClassifiedPath(url, getAddress(event))}>
|
||||||
{#if title}
|
{#if title}
|
||||||
<div class="flex w-full items-center justify-between gap-2">
|
<div class="flex w-full items-center justify-between gap-2">
|
||||||
<p class="text-xl">{title}</p>
|
<p class="text-xl">{title}</p>
|
||||||
|
|||||||
@@ -10,14 +10,16 @@
|
|||||||
const props: ComponentProps<typeof Content> = $props()
|
const props: ComponentProps<typeof Content> = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if props.event.kind === EVENT_TIME}
|
{#key props.event.id}
|
||||||
<NoteContentEventTime {...props} />
|
{#if props.event.kind === EVENT_TIME}
|
||||||
{:else if props.event.kind === THREAD}
|
<NoteContentEventTime {...props} />
|
||||||
<NoteContentThread {...props} />
|
{:else if props.event.kind === THREAD}
|
||||||
{:else if props.event.kind === CLASSIFIED}
|
<NoteContentThread {...props} />
|
||||||
<NoteContentClassified {...props} />
|
{:else if props.event.kind === CLASSIFIED}
|
||||||
{:else if props.event.kind === ZAP_GOAL}
|
<NoteContentClassified {...props} />
|
||||||
<NoteContentGoal {...props} />
|
{:else if props.event.kind === ZAP_GOAL}
|
||||||
{:else}
|
<NoteContentGoal {...props} />
|
||||||
<Content {...props} />
|
{:else}
|
||||||
{/if}
|
<Content {...props} />
|
||||||
|
{/if}
|
||||||
|
{/key}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
{#if props.event.content}
|
{#if props.event.content}
|
||||||
<Content {...props} />
|
<Content {...props} />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="grid grid-cols-3 sm:grid-cols-5">
|
<div class="grid grid-cols-3 sm:grid-cols-5 lg:grid-cols-9">
|
||||||
{#each images as image (image)}
|
{#each images as image (image)}
|
||||||
<ContentLinkBlock event={props.event} value={{url: image}} />
|
<ContentLinkBlock event={props.event} value={{url: image}} />
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -531,7 +531,6 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
|
|||||||
|
|
||||||
const ext = "." + type.split("/")[1]
|
const ext = "." + type.split("/")[1]
|
||||||
const server = await getBlossomServer(options)
|
const server = await getBlossomServer(options)
|
||||||
console.log("====", server)
|
|
||||||
const hashes = [await sha256(await file.arrayBuffer())]
|
const hashes = [await sha256(await file.arrayBuffer())]
|
||||||
const $signer = signer.get() || Nip01Signer.ephemeral()
|
const $signer = signer.get() || Nip01Signer.ephemeral()
|
||||||
const authTemplate = makeBlossomAuthEvent({action: "upload", server, hashes})
|
const authTemplate = makeBlossomAuthEvent({action: "upload", server, hashes})
|
||||||
|
|||||||
@@ -232,13 +232,13 @@ export const notifications = derived(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const commentsByClassifiedId = groupBy(
|
const commentsByClassifiedAddress = groupBy(
|
||||||
e => getTagValue("E", e.tags),
|
e => getTagValue("A", e.tags),
|
||||||
classifiedComments.filter(spec({kind: COMMENT})),
|
classifiedComments.filter(spec({kind: COMMENT})),
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const [classifiedId, [comment]] of commentsByClassifiedId.entries()) {
|
for (const [address, [comment]] of commentsByClassifiedAddress.entries()) {
|
||||||
const classifiedItemPath = makeClassifiedPath(url, classifiedId)
|
const classifiedItemPath = makeClassifiedPath(url, address)
|
||||||
|
|
||||||
if (hasNotification(classifiedPath, comment)) {
|
if (hasNotification(classifiedPath, comment)) {
|
||||||
paths.add(spacePathMobile)
|
paths.add(spacePathMobile)
|
||||||
@@ -250,13 +250,13 @@ export const notifications = derived(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const commentsByEventId = groupBy(
|
const commentsByEventAddress = groupBy(
|
||||||
e => getTagValue("E", e.tags),
|
e => getTagValue("A", e.tags),
|
||||||
calendarComments.filter(spec({kind: COMMENT})),
|
calendarComments.filter(spec({kind: COMMENT})),
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const [eventId, [comment]] of commentsByEventId.entries()) {
|
for (const [address, [comment]] of commentsByEventAddress.entries()) {
|
||||||
const calendarItemPath = makeCalendarPath(url, eventId)
|
const calendarItemPath = makeCalendarPath(url, address)
|
||||||
|
|
||||||
if (hasNotification(calendarPath, comment)) {
|
if (hasNotification(calendarPath, comment)) {
|
||||||
paths.add(spacePathMobile)
|
paths.add(spacePathMobile)
|
||||||
|
|||||||
+20
-19
@@ -62,16 +62,13 @@ export const makeRoomPath = (url: string, h: string) => `/spaces/${encodeRelay(u
|
|||||||
|
|
||||||
export const makeSpaceChatPath = (url: string) => makeRoomPath(url, "chat")
|
export const makeSpaceChatPath = (url: string) => makeRoomPath(url, "chat")
|
||||||
|
|
||||||
export const makeGoalPath = (url: string, eventId?: string) => makeSpacePath(url, "goals", eventId)
|
export const makeGoalPath = (url: string, id?: string) => makeSpacePath(url, "goals", id)
|
||||||
|
|
||||||
export const makeThreadPath = (url: string, eventId?: string) =>
|
export const makeThreadPath = (url: string, id?: string) => makeSpacePath(url, "threads", id)
|
||||||
makeSpacePath(url, "threads", eventId)
|
|
||||||
|
|
||||||
export const makeClassifiedPath = (url: string, eventId?: string) =>
|
export const makeClassifiedPath = (url: string, address?: string) => makeSpacePath(url, "classifieds", address)
|
||||||
makeSpacePath(url, "classifieds", eventId)
|
|
||||||
|
|
||||||
export const makeCalendarPath = (url: string, eventId?: string) =>
|
export const makeCalendarPath = (url: string, address?: string) => makeSpacePath(url, "calendar", address)
|
||||||
makeSpacePath(url, "calendar", eventId)
|
|
||||||
|
|
||||||
export const getPrimaryNavItem = ($page: Page) => $page.route?.id?.split("/")[1]
|
export const getPrimaryNavItem = ($page: Page) => $page.route?.id?.split("/")[1]
|
||||||
|
|
||||||
@@ -126,17 +123,18 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === CLASSIFIED) {
|
if (event.kind === CLASSIFIED) {
|
||||||
return makeClassifiedPath(url, event.id)
|
return makeClassifiedPath(url, getAddress(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === EVENT_TIME) {
|
if (event.kind === EVENT_TIME) {
|
||||||
return makeCalendarPath(url, event.id)
|
return makeCalendarPath(url, getAddress(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === MESSAGE) {
|
if (event.kind === MESSAGE) {
|
||||||
return h ? makeRoomPath(url, h) : makeSpacePath(url, "chat")
|
return h ? makeRoomPath(url, h) : makeSpacePath(url, "chat")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const address = event.tags.find(nthEq(0, "A"))?.[1]
|
||||||
const kind = event.tags.find(nthEq(0, "K"))?.[1]
|
const kind = event.tags.find(nthEq(0, "K"))?.[1]
|
||||||
const id = event.tags.find(nthEq(0, "E"))?.[1]
|
const id = event.tags.find(nthEq(0, "E"))?.[1]
|
||||||
|
|
||||||
@@ -149,18 +147,21 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
|||||||
return makeThreadPath(url, id)
|
return makeThreadPath(url, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseInt(kind) === CLASSIFIED) {
|
|
||||||
return makeClassifiedPath(url, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parseInt(kind) === EVENT_TIME) {
|
|
||||||
return makeCalendarPath(url, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parseInt(kind) === MESSAGE) {
|
if (parseInt(kind) === MESSAGE) {
|
||||||
return h ? makeRoomPath(url, h) : makeSpacePath(url, "chat")
|
return h ? makeRoomPath(url, h) : makeSpacePath(url, "chat")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (address && kind) {
|
||||||
|
if (parseInt(kind) === CLASSIFIED) {
|
||||||
|
return makeClassifiedPath(url, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseInt(kind) === EVENT_TIME) {
|
||||||
|
return makeCalendarPath(url, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return entityLink(nip19.neventEncode({id: event.id, relays: urls}))
|
return entityLink(nip19.neventEncode({id: event.id, relays: urls}))
|
||||||
@@ -171,10 +172,10 @@ export const getRoomItemPath = (url: string, event: TrustedEvent) => {
|
|||||||
case THREAD:
|
case THREAD:
|
||||||
return makeThreadPath(url, event.id)
|
return makeThreadPath(url, event.id)
|
||||||
case CLASSIFIED:
|
case CLASSIFIED:
|
||||||
return makeClassifiedPath(url, event.id)
|
return makeClassifiedPath(url, getAddress(event))
|
||||||
case ZAP_GOAL:
|
case ZAP_GOAL:
|
||||||
return makeGoalPath(url, event.id)
|
return makeGoalPath(url, event.id)
|
||||||
case EVENT_TIME:
|
case EVENT_TIME:
|
||||||
return makeCalendarPath(url, event.id)
|
return makeCalendarPath(url, getAddress(event))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-8
@@ -24,10 +24,10 @@
|
|||||||
import {deriveEvent, decodeRelay} from "@app/core/state"
|
import {deriveEvent, decodeRelay} from "@app/core/state"
|
||||||
import {setChecked} from "@app/util/notifications"
|
import {setChecked} from "@app/util/notifications"
|
||||||
|
|
||||||
const {relay, id} = $page.params as MakeNonOptional<typeof $page.params>
|
const {relay, address} = $page.params as MakeNonOptional<typeof $page.params>
|
||||||
const url = decodeRelay(relay)
|
const url = decodeRelay(relay)
|
||||||
const event = deriveEvent(id, [url])
|
const event = deriveEvent(address, [url])
|
||||||
const filters = [{kinds: [COMMENT], "#E": [id]}]
|
const filters = [{kinds: [COMMENT], "#A": [address]}]
|
||||||
const replies = deriveEventsAsc(deriveEventsById({filters, repository}))
|
const replies = deriveEventsAsc(deriveEventsById({filters, repository}))
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
@@ -115,10 +115,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
{#await sleep(5000)}
|
<div class="py-20 m-auto">
|
||||||
<Spinner loading>Loading listing...</Spinner>
|
{#await sleep(5000)}
|
||||||
{:then}
|
<Spinner loading>Loading listing...</Spinner>
|
||||||
<p>Failed to load classified listing.</p>
|
{:then}
|
||||||
{/await}
|
<p>Failed to load classified listing.</p>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
<Icon icon={AltArrowRight} />
|
<Icon icon={AltArrowRight} />
|
||||||
</Link>
|
</Link>
|
||||||
{:else if event.kind === CLASSIFIED}
|
{:else if event.kind === CLASSIFIED}
|
||||||
<Link href={makeClassifiedPath(url, event.id)} class="btn btn-primary btn-sm">
|
<Link href={makeClassifiedPath(url, getAddress(event))} class="btn btn-primary btn-sm">
|
||||||
View Listing
|
View Listing
|
||||||
<Icon icon={AltArrowRight} />
|
<Icon icon={AltArrowRight} />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
<Icon icon={AltArrowRight} />
|
<Icon icon={AltArrowRight} />
|
||||||
</Link>
|
</Link>
|
||||||
{:else if event.kind === EVENT_TIME}
|
{:else if event.kind === EVENT_TIME}
|
||||||
<Link href={makeCalendarPath(url, event.id)} class="btn btn-primary btn-sm">
|
<Link href={makeCalendarPath(url, getAddress(event))} class="btn btn-primary btn-sm">
|
||||||
View Event
|
View Event
|
||||||
<Icon icon={AltArrowRight} />
|
<Icon icon={AltArrowRight} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
Reference in New Issue
Block a user