Add topics to classifieds
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {uniq} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {getTagValue, getAddress} from "@welshman/util"
|
import {getTagValue, getTagValues, getAddress} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
||||||
|
import {normalizeTopic} from '@lib/util'
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
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"
|
||||||
@@ -27,6 +29,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 topics = getTagValues("t", event.tags)
|
||||||
const path = makeClassifiedPath(url, getAddress(event))
|
const path = makeClassifiedPath(url, getAddress(event))
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
@@ -45,6 +48,13 @@
|
|||||||
Posted in #<RoomName {h} {url} />
|
Posted in #<RoomName {h} {url} />
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="flex min-w-0 flex-wrap gap-2">
|
||||||
|
{#each uniq(topics) as topic (topic)}
|
||||||
|
<button type="button" class="btn btn-xs rounded-full font-normal">
|
||||||
|
#{normalizeTopic(topic)}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||||
<ThunkStatusOrDeleted {event}>
|
<ThunkStatusOrDeleted {event}>
|
||||||
<ClassifiedStatus {event} />
|
<ClassifiedStatus {event} />
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
const {d, title, status} = fromPairs(event.tags)
|
const {d, title, status} = fromPairs(event.tags)
|
||||||
const [_, price = 0, currency = "SAT"] = getTag("price", event.tags) || []
|
const [_, price = 0, currency = "SAT"] = getTag("price", event.tags) || []
|
||||||
const images = getTagValues("image", event.tags)
|
const images = getTagValues("image", event.tags)
|
||||||
const initialValues = {d, title, status, content, price: Number(price), currency, images}
|
const topics = getTagValues("t", event.tags)
|
||||||
|
const initialValues = {d, title, status, content, price: Number(price), currency, images, topics}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ClassifiedForm {url} {initialValues}>
|
<ClassifiedForm {url} {initialValues}>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {randomId} from "@welshman/lib"
|
import {removeUndefined, randomId, uniq} from "@welshman/lib"
|
||||||
import {makeEvent, CLASSIFIED} from "@welshman/util"
|
import {makeEvent, CLASSIFIED} from "@welshman/util"
|
||||||
import {publishThunk} from "@welshman/app"
|
import {publishThunk} from "@welshman/app"
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
|
import {normalizeTopic} from "@lib/util"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import ImagesInput from "@lib/components/ImagesInput.svelte"
|
import ImagesInput from "@lib/components/ImagesInput.svelte"
|
||||||
import CurrencyInput from "@app/components/CurrencyInput.svelte"
|
import CurrencyInput from "@app/components/CurrencyInput.svelte"
|
||||||
|
import TopicMultiSelect from "@app/components/TopicMultiSelect.svelte"
|
||||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {PROTECTED} from "@app/core/state"
|
import {PROTECTED} from "@app/core/state"
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
currency?: string
|
currency?: string
|
||||||
images?: string[]
|
images?: string[]
|
||||||
status?: string
|
status?: string
|
||||||
|
topics?: string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +74,10 @@
|
|||||||
...ed.storage.nostr.getEditorTags(),
|
...ed.storage.nostr.getEditorTags(),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
for (const topic of topics) {
|
||||||
|
tags.push(["t", topic])
|
||||||
|
}
|
||||||
|
|
||||||
if (await shouldProtect) {
|
if (await shouldProtect) {
|
||||||
tags.push(PROTECTED)
|
tags.push(PROTECTED)
|
||||||
}
|
}
|
||||||
@@ -118,6 +125,7 @@
|
|||||||
let price = $state(Number(initialValues?.price || 0))
|
let price = $state(Number(initialValues?.price || 0))
|
||||||
let currency = $state(initialValues?.currency || "SAT")
|
let currency = $state(initialValues?.currency || "SAT")
|
||||||
let images = $state<(string | File)[]>(initialValues?.images || [])
|
let images = $state<(string | File)[]>(initialValues?.images || [])
|
||||||
|
let topics = $state(uniq(removeUndefined((initialValues?.topics || []).map(normalizeTopic))))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
||||||
@@ -150,6 +158,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Topics</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<TopicMultiSelect bind:value={topics} />
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
<p>Price*</p>
|
<p>Price*</p>
|
||||||
|
|||||||
@@ -96,6 +96,10 @@
|
|||||||
params={{
|
params={{
|
||||||
trigger: "manual",
|
trigger: "manual",
|
||||||
interactive: true,
|
interactive: true,
|
||||||
|
placement: "bottom",
|
||||||
getReferenceClientRect: () => wrapper!.getBoundingClientRect(),
|
getReferenceClientRect: () => wrapper!.getBoundingClientRect(),
|
||||||
|
onShow: (instance: Instance) => {
|
||||||
|
instance.popper.style.width = `${wrapper!.getBoundingClientRect().width + 8}px`
|
||||||
|
},
|
||||||
}} />
|
}} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let input: Element | undefined = $state()
|
let label: Element | undefined = $state()
|
||||||
let popover: Instance | undefined = $state()
|
let popover: Instance | undefined = $state()
|
||||||
let instance: any = $state()
|
let instance: any = $state()
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<label class="input input-bordered flex w-full items-center gap-2" bind:this={input}>
|
<label class="input input-bordered flex w-full items-center gap-2" bind:this={label}>
|
||||||
<Icon icon={Magnifier} />
|
<Icon icon={Magnifier} />
|
||||||
<!-- svelte-ignore a11y_autofocus -->
|
<!-- svelte-ignore a11y_autofocus -->
|
||||||
<input
|
<input
|
||||||
@@ -114,12 +114,15 @@
|
|||||||
select: selectPubkey,
|
select: selectPubkey,
|
||||||
component: ProfileSuggestion,
|
component: ProfileSuggestion,
|
||||||
class: "rounded-box",
|
class: "rounded-box",
|
||||||
style: `left: 4px; width: ${input?.clientWidth + 12}px`,
|
style: `left: 4px; width: ${label?.clientWidth + 12}px`,
|
||||||
}}
|
}}
|
||||||
params={{
|
params={{
|
||||||
trigger: "manual",
|
trigger: "manual",
|
||||||
interactive: true,
|
interactive: true,
|
||||||
maxWidth: "none",
|
placement: "bottom",
|
||||||
getReferenceClientRect: () => input!.getBoundingClientRect(),
|
getReferenceClientRect: () => label!.getBoundingClientRect(),
|
||||||
|
onShow: (instance: Instance) => {
|
||||||
|
instance.popper.style.width = `${label!.getBoundingClientRect().width + 8}px`
|
||||||
|
},
|
||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {writable} from "svelte/store"
|
||||||
|
import type {Writable} from "svelte/store"
|
||||||
|
import type {Instance} from "tippy.js"
|
||||||
|
import {remove, without, uniq} from "@welshman/lib"
|
||||||
|
import {createSearch, topics} from "@welshman/app"
|
||||||
|
import {normalizeTopic} from "@lib/util"
|
||||||
|
import Suggestions from "@lib/components/Suggestions.svelte"
|
||||||
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import TopicSuggestion from "@app/components/TopicSuggestion.svelte"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: string[]
|
||||||
|
term?: Writable<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
let {value = $bindable(), term = writable("")}: Props = $props()
|
||||||
|
|
||||||
|
const topicSearch = $derived.by(() =>
|
||||||
|
createSearch(without(value, $topics), {
|
||||||
|
getValue: topic => topic.name,
|
||||||
|
fuseOptions: {
|
||||||
|
keys: ["name"],
|
||||||
|
threshold: 0.4,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const addTopic = (text: string) => {
|
||||||
|
const topic = normalizeTopic(text)
|
||||||
|
|
||||||
|
if (topic) {
|
||||||
|
value = uniq([...value, topic])
|
||||||
|
}
|
||||||
|
|
||||||
|
term.set("")
|
||||||
|
popover?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeTopic = (topic: string) => {
|
||||||
|
value = remove(topic, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (instance?.onKeyDown(e)) {
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "Enter" && $term) {
|
||||||
|
e.preventDefault()
|
||||||
|
addTopic($term)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBlur = () => {
|
||||||
|
term.set("")
|
||||||
|
popover?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
let label: Element | undefined = $state()
|
||||||
|
let popover: Instance | undefined = $state()
|
||||||
|
let instance: any = $state()
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($term.trim()) {
|
||||||
|
popover?.show()
|
||||||
|
} else {
|
||||||
|
popover?.hide()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#each value as topic (topic)}
|
||||||
|
<div class="badge badge-neutral gap-1">
|
||||||
|
<Button class="flex items-center" onclick={() => removeTopic(topic)}>
|
||||||
|
<Icon icon={CloseCircle} size={4} class="-ml-1 mt-px" />
|
||||||
|
</Button>
|
||||||
|
<span>#{topic}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2" bind:this={label}>
|
||||||
|
<Icon icon={Magnifier} />
|
||||||
|
<input
|
||||||
|
bind:value={$term}
|
||||||
|
class="grow"
|
||||||
|
type="text"
|
||||||
|
placeholder="Add topics..."
|
||||||
|
onkeydown={onKeyDown}
|
||||||
|
onblur={onBlur} />
|
||||||
|
</label>
|
||||||
|
<Tippy
|
||||||
|
bind:popover
|
||||||
|
bind:instance
|
||||||
|
component={Suggestions}
|
||||||
|
props={{
|
||||||
|
term,
|
||||||
|
search: topicSearch.searchValues,
|
||||||
|
select: addTopic,
|
||||||
|
component: TopicSuggestion,
|
||||||
|
allowCreate: true,
|
||||||
|
}}
|
||||||
|
params={{
|
||||||
|
trigger: "manual",
|
||||||
|
interactive: true,
|
||||||
|
placement: "bottom",
|
||||||
|
getReferenceClientRect: () => label!.getBoundingClientRect(),
|
||||||
|
onShow: (instance: Instance) => {
|
||||||
|
instance.popper.style.width = `${label!.getBoundingClientRect().width + 8}px`
|
||||||
|
},
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
type Props = {
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {value}: Props = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span>#{value}</span>
|
||||||
@@ -28,3 +28,5 @@ export const buildUrl = (base: string | URL, ...pathname: string[]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const addPeriod = (s: string) => (s + ".").replace(/\.+$/, ".")
|
export const addPeriod = (s: string) => (s + ".").replace(/\.+$/, ".")
|
||||||
|
|
||||||
|
export const normalizeTopic = (topic: string) => topic.trim().replace(/^#+/, "").toLowerCase()
|
||||||
|
|||||||
Reference in New Issue
Block a user