Add image uploads to classifieds
This commit is contained in:
@@ -1,25 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {writable} from "svelte/store"
|
|
||||||
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 Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
|
||||||
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"
|
||||||
import Button from "@lib/components/Button.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 ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import Modal from "@lib/components/Modal.svelte"
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ImagesInput from "@lib/components/ImagesInput.svelte"
|
||||||
import CurrencyInput from "@app/components/CurrencyInput.svelte"
|
import CurrencyInput from "@app/components/CurrencyInput.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"
|
||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {canEnforceNip70} from "@app/core/commands"
|
import {canEnforceNip70, uploadFile} from "@app/core/commands"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@@ -30,14 +30,10 @@
|
|||||||
|
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
const uploading = writable(false)
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading) return
|
loading = true
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
@@ -52,33 +48,62 @@
|
|||||||
if (!content.trim()) {
|
if (!content.trim()) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Please provide a message for your listing.",
|
message: "Please provide a description for your listing.",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = [...ed.storage.nostr.getEditorTags(), ["title", title]]
|
const tags = [...ed.storage.nostr.getEditorTags(), ["summary", content], ["title", title]]
|
||||||
|
|
||||||
if (await shouldProtect) {
|
try {
|
||||||
tags.push(PROTECTED)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
if (h) {
|
|
||||||
tags.push(["h", h])
|
|
||||||
}
|
|
||||||
|
|
||||||
publishThunk({
|
|
||||||
relays: [url],
|
|
||||||
event: makeEvent(CLASSIFIED, {content, tags}),
|
|
||||||
})
|
|
||||||
|
|
||||||
history.back()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const editor = makeEditor({url, submit, uploading, placeholder: "What's on your mind?"})
|
const editor = makeEditor({
|
||||||
|
url,
|
||||||
|
submit,
|
||||||
|
placeholder: "Provide a detailed description for your listing.",
|
||||||
|
})
|
||||||
|
|
||||||
let title = $state("")
|
let title = $state("")
|
||||||
|
let loading = $state(false)
|
||||||
let currencyCode = $state("SAT")
|
let currencyCode = $state("SAT")
|
||||||
let currencyAmount = $state(0)
|
let currencyAmount = $state(0)
|
||||||
|
let images = $state<(string | File)[]>([])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
||||||
@@ -129,29 +154,21 @@
|
|||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
<p>Images</p>
|
<p>Images (optional)</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet input()}
|
{#snippet input()}
|
||||||
todo: attach multiple images
|
<ImagesInput bind:value={images} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Field>
|
</Field>
|
||||||
<Button
|
|
||||||
data-tip="Add an image"
|
|
||||||
class="tooltip tooltip-left absolute bottom-1 right-2"
|
|
||||||
onclick={selectFiles}>
|
|
||||||
{#if $uploading}
|
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
|
||||||
{:else}
|
|
||||||
<Icon icon={Paperclip} size={3} />
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary">Create Listing</Button>
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
<Spinner {loading}>Create Listing</Spinner>
|
||||||
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {formatTimestamp} from "@welshman/lib"
|
import {formatTimestamp} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {getTagValue} from "@welshman/util"
|
import {getTagValue, getTagValues} from "@welshman/util"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import ContentLinkBlock from "@app/components/ContentLinkBlock.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||||
import ClassifiedActions from "@app/components/ClassifiedActions.svelte"
|
import ClassifiedActions from "@app/components/ClassifiedActions.svelte"
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
|
|
||||||
const title = getTagValue("title", event.tags)
|
const title = getTagValue("title", event.tags)
|
||||||
const h = getTagValue("h", event.tags)
|
const h = getTagValue("h", event.tags)
|
||||||
|
const images = getTagValues("image", event.tags)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
@@ -36,6 +38,11 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<Content {event} {url} expandMode="inline" />
|
<Content {event} {url} expandMode="inline" />
|
||||||
|
<div class="grid grid-cols-3 sm:grid-cols-5">
|
||||||
|
{#each images as image (image)}
|
||||||
|
<ContentLinkBlock {event} value={{url: image}} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
<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">
|
||||||
Posted by
|
Posted by
|
||||||
|
|||||||
@@ -186,7 +186,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{:else if isEllipsis(parsed) && expandInline}
|
{:else if isEllipsis(parsed) && expandInline}
|
||||||
{@html renderAsHtml(parsed)}
|
{@html renderAsHtml(parsed)}
|
||||||
<button type="button" class="text-sm underline"> Read more </button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-sm underline"
|
||||||
|
onclick={stopPropagation(preventDefault(expand))}>
|
||||||
|
Read more
|
||||||
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
{@html renderAsHtml(parsed)}
|
{@html renderAsHtml(parsed)}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import type {Writable} from "svelte/store"
|
import type {Writable} from "svelte/store"
|
||||||
import type {Instance} from "tippy.js"
|
import type {Instance} from "tippy.js"
|
||||||
import {preventDefault} from '@lib/html'
|
import {preventDefault} from "@lib/html"
|
||||||
import {createSearch} from "@welshman/app"
|
import {createSearch} from "@welshman/app"
|
||||||
import {currencyOptions, displayCurrency} from "@lib/currency"
|
import {currencyOptions, displayCurrency} from "@lib/currency"
|
||||||
import Suggestions from "@lib/components/Suggestions.svelte"
|
import Suggestions from "@lib/components/Suggestions.svelte"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
import Paperclip from "@assets/icons/paperclip-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 ModalFooter from "@lib/components/ModalFooter.svelte"
|
|
||||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||||
import {publishComment, canEnforceNip70} from "@app/core/commands"
|
import {publishComment, canEnforceNip70} from "@app/core/commands"
|
||||||
import {PROTECTED} from "@app/core/state"
|
import {PROTECTED} from "@app/core/state"
|
||||||
@@ -65,12 +64,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={spacer}></div>
|
<div bind:this={spacer}></div>
|
||||||
<form
|
<form in:fly bind:this={form} onsubmit={preventDefault(submit)} class="cb cw fixed z-feature pt-3">
|
||||||
in:fly
|
<div class="card2 mx-2 my-2 bg-alt shadow-md">
|
||||||
bind:this={form}
|
|
||||||
onsubmit={preventDefault(submit)}
|
|
||||||
class="cb cw fixed z-feature -mx-2 pt-3">
|
|
||||||
<div class="card2 mx-2 my-2 bg-neutral">
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="note-editor flex-grow overflow-hidden">
|
<div class="note-editor flex-grow overflow-hidden">
|
||||||
<EditorContent {editor} />
|
<EditorContent {editor} />
|
||||||
@@ -86,9 +81,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ModalFooter>
|
<div class="flex justify-between pt-3">
|
||||||
<Button class="btn btn-link" onclick={onClose}>Cancel</Button>
|
<Button class="btn btn-link" onclick={onClose}>Cancel</Button>
|
||||||
<Button type="submit" class="btn btn-primary">Post Reply</Button>
|
<Button type="submit" class="btn btn-primary">Post Reply</Button>
|
||||||
</ModalFooter>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import CardButton from "@lib/components/CardButton.svelte"
|
import CardButton from "@lib/components/CardButton.svelte"
|
||||||
import LogOut from "@app/components/LogOut.svelte"
|
import LogOut from "@app/components/LogOut.svelte"
|
||||||
import {PLATFORM_NAME} from "@app/core/state"
|
import {PLATFORM_NAME} from "@app/core/state"
|
||||||
@@ -21,99 +23,101 @@
|
|||||||
const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark")
|
const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark")
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="column menu gap-2">
|
<Modal>
|
||||||
<Link replaceState href="/settings/profile">
|
<ModalBody>
|
||||||
<CardButton class="btn-neutral">
|
<Link replaceState href="/settings/profile">
|
||||||
{#snippet icon()}
|
<CardButton class="btn-neutral">
|
||||||
<div><Icon icon={UserRounded} size={7} /></div>
|
{#snippet icon()}
|
||||||
{/snippet}
|
<div><Icon icon={UserRounded} size={7} /></div>
|
||||||
{#snippet title()}
|
{/snippet}
|
||||||
<div>Profile</div>
|
{#snippet title()}
|
||||||
{/snippet}
|
<div>Profile</div>
|
||||||
{#snippet info()}
|
{/snippet}
|
||||||
<div>Customize your user profile</div>
|
{#snippet info()}
|
||||||
{/snippet}
|
<div>Customize your user profile</div>
|
||||||
</CardButton>
|
{/snippet}
|
||||||
</Link>
|
</CardButton>
|
||||||
<Link replaceState href="/settings/alerts">
|
</Link>
|
||||||
<CardButton class="btn-neutral">
|
<Link replaceState href="/settings/alerts">
|
||||||
{#snippet icon()}
|
<CardButton class="btn-neutral">
|
||||||
<div><Icon icon={Bell} size={7} /></div>
|
{#snippet icon()}
|
||||||
{/snippet}
|
<div><Icon icon={Bell} size={7} /></div>
|
||||||
{#snippet title()}
|
{/snippet}
|
||||||
<div>Alerts</div>
|
{#snippet title()}
|
||||||
{/snippet}
|
<div>Alerts</div>
|
||||||
{#snippet info()}
|
{/snippet}
|
||||||
<div>Set up email digests and push notifications</div>
|
{#snippet info()}
|
||||||
{/snippet}
|
<div>Set up email digests and push notifications</div>
|
||||||
</CardButton>
|
{/snippet}
|
||||||
</Link>
|
</CardButton>
|
||||||
<Link replaceState href="/settings/wallet">
|
</Link>
|
||||||
<CardButton class="btn-neutral">
|
<Link replaceState href="/settings/wallet">
|
||||||
{#snippet icon()}
|
<CardButton class="btn-neutral">
|
||||||
<div><Icon icon={Wallet} size={7} /></div>
|
{#snippet icon()}
|
||||||
{/snippet}
|
<div><Icon icon={Wallet} size={7} /></div>
|
||||||
{#snippet title()}
|
{/snippet}
|
||||||
<div>Wallet</div>
|
{#snippet title()}
|
||||||
{/snippet}
|
<div>Wallet</div>
|
||||||
{#snippet info()}
|
{/snippet}
|
||||||
<div>Connect a bitcoin wallet for sending social tips</div>
|
{#snippet info()}
|
||||||
{/snippet}
|
<div>Connect a bitcoin wallet for sending social tips</div>
|
||||||
</CardButton>
|
{/snippet}
|
||||||
</Link>
|
</CardButton>
|
||||||
<Link replaceState href="/settings/relays">
|
</Link>
|
||||||
<CardButton class="btn-neutral">
|
<Link replaceState href="/settings/relays">
|
||||||
{#snippet icon()}
|
<CardButton class="btn-neutral">
|
||||||
<div><Icon icon={Server} size={7} /></div>
|
{#snippet icon()}
|
||||||
{/snippet}
|
<div><Icon icon={Server} size={7} /></div>
|
||||||
{#snippet title()}
|
{/snippet}
|
||||||
<div>Relays</div>
|
{#snippet title()}
|
||||||
{/snippet}
|
<div>Relays</div>
|
||||||
{#snippet info()}
|
{/snippet}
|
||||||
<div>Control how {PLATFORM_NAME} talks to the network</div>
|
{#snippet info()}
|
||||||
{/snippet}
|
<div>Control how {PLATFORM_NAME} talks to the network</div>
|
||||||
</CardButton>
|
{/snippet}
|
||||||
</Link>
|
</CardButton>
|
||||||
<Link replaceState href="/settings/content">
|
</Link>
|
||||||
<CardButton class="btn-neutral">
|
<Link replaceState href="/settings/content">
|
||||||
{#snippet icon()}
|
<CardButton class="btn-neutral">
|
||||||
<div><Icon icon={Settings} size={7} /></div>
|
{#snippet icon()}
|
||||||
{/snippet}
|
<div><Icon icon={Settings} size={7} /></div>
|
||||||
{#snippet title()}
|
{/snippet}
|
||||||
<div>Settings</div>
|
{#snippet title()}
|
||||||
{/snippet}
|
<div>Settings</div>
|
||||||
{#snippet info()}
|
{/snippet}
|
||||||
<div>Get into the details about how {PLATFORM_NAME} works</div>
|
{#snippet info()}
|
||||||
{/snippet}
|
<div>Get into the details about how {PLATFORM_NAME} works</div>
|
||||||
</CardButton>
|
{/snippet}
|
||||||
</Link>
|
</CardButton>
|
||||||
<Button onclick={toggleTheme}>
|
</Link>
|
||||||
<CardButton class="btn-neutral">
|
<Button onclick={toggleTheme}>
|
||||||
{#snippet icon()}
|
<CardButton class="btn-neutral">
|
||||||
<div><Icon icon={Moon} size={7} /></div>
|
{#snippet icon()}
|
||||||
{/snippet}
|
<div><Icon icon={Moon} size={7} /></div>
|
||||||
{#snippet title()}
|
{/snippet}
|
||||||
<div>Theme</div>
|
{#snippet title()}
|
||||||
{/snippet}
|
<div>Theme</div>
|
||||||
{#snippet info()}
|
{/snippet}
|
||||||
<div>Switch between light and dark mode</div>
|
{#snippet info()}
|
||||||
{/snippet}
|
<div>Switch between light and dark mode</div>
|
||||||
</CardButton>
|
{/snippet}
|
||||||
</Button>
|
</CardButton>
|
||||||
<Link replaceState href="/settings/about">
|
</Button>
|
||||||
<CardButton class="btn-neutral">
|
<Link replaceState href="/settings/about">
|
||||||
{#snippet icon()}
|
<CardButton class="btn-neutral">
|
||||||
<div><Icon icon={Code2} size={7} /></div>
|
{#snippet icon()}
|
||||||
{/snippet}
|
<div><Icon icon={Code2} size={7} /></div>
|
||||||
{#snippet title()}
|
{/snippet}
|
||||||
<div>About</div>
|
{#snippet title()}
|
||||||
{/snippet}
|
<div>About</div>
|
||||||
{#snippet info()}
|
{/snippet}
|
||||||
<div>Learn about {PLATFORM_NAME} and support the developer</div>
|
{#snippet info()}
|
||||||
{/snippet}
|
<div>Learn about {PLATFORM_NAME} and support the developer</div>
|
||||||
</CardButton>
|
{/snippet}
|
||||||
</Link>
|
</CardButton>
|
||||||
<Button onclick={logout} class="btn btn-neutral">
|
</Link>
|
||||||
<Icon icon={Exit} /> Log Out
|
<Button onclick={logout} class="btn btn-neutral">
|
||||||
</Button>
|
<Icon icon={Exit} /> Log Out
|
||||||
</div>
|
</Button>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {ComponentProps} from "svelte"
|
import type {ComponentProps} from "svelte"
|
||||||
import {getTagValue} from "@welshman/util"
|
import {getTagValue, getTagValues} from "@welshman/util"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
|
import ContentLinkBlock from "@app/components/ContentLinkBlock.svelte"
|
||||||
|
|
||||||
const props: ComponentProps<typeof Content> = $props()
|
const props: ComponentProps<typeof Content> = $props()
|
||||||
|
|
||||||
const title = getTagValue("title", props.event.tags)
|
const title = getTagValue("title", props.event.tags)
|
||||||
|
const images = getTagValues("image", props.event.tags)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
@@ -15,4 +17,9 @@
|
|||||||
{#if props.event.content}
|
{#if props.event.content}
|
||||||
<Content {...props} />
|
<Content {...props} />
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="grid grid-cols-3 sm:grid-cols-5">
|
||||||
|
{#each images as image (image)}
|
||||||
|
<ContentLinkBlock event={props.event} value={{url: image}} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
toNostrURI,
|
toNostrURI,
|
||||||
RelayMode,
|
RelayMode,
|
||||||
getTagValues,
|
getTagValues,
|
||||||
|
uploadBlob,
|
||||||
canUploadBlob,
|
canUploadBlob,
|
||||||
encryptFile,
|
encryptFile,
|
||||||
makeBlossomAuthEvent,
|
makeBlossomAuthEvent,
|
||||||
@@ -50,7 +51,6 @@ import {
|
|||||||
editProfile,
|
editProfile,
|
||||||
createProfile,
|
createProfile,
|
||||||
uniqTags,
|
uniqTags,
|
||||||
makeHttpAuthHeader,
|
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
|
import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
|
||||||
import {Router} from "@welshman/router"
|
import {Router} from "@welshman/router"
|
||||||
@@ -509,8 +509,6 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
|
|||||||
try {
|
try {
|
||||||
const {name, type} = file
|
const {name, type} = file
|
||||||
|
|
||||||
console.log("======== 1", name, type, options.encrypt)
|
|
||||||
|
|
||||||
if (!type.match("image/(webp|gif|svg)")) {
|
if (!type.match("image/(webp|gif|svg)")) {
|
||||||
file = await compressFile(file, options)
|
file = await compressFile(file, options)
|
||||||
}
|
}
|
||||||
@@ -532,25 +530,14 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ext = "." + type.split("/")[1]
|
const ext = "." + type.split("/")[1]
|
||||||
console.log("======== 2", ext)
|
|
||||||
const server = await getBlossomServer(options)
|
const server = await getBlossomServer(options)
|
||||||
console.log("======== 3", server)
|
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})
|
||||||
const authEvent = await $signer.sign(authTemplate)
|
const authEvent = await $signer.sign(authTemplate)
|
||||||
console.log("======== 4", authEvent.id)
|
const res = await uploadBlob(server, file, {authEvent})
|
||||||
// const res = await uploadBlob(server, file, {authEvent})
|
|
||||||
|
|
||||||
const res = await fetch(`${new URL(server).origin}/upload`, {
|
|
||||||
method: "PUT",
|
|
||||||
headers: {Authorization: makeHttpAuthHeader(authEvent)},
|
|
||||||
body: file instanceof Blob ? file : new Blob([file]),
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log("======== 5", res.status)
|
|
||||||
const text = await res.text()
|
const text = await res.text()
|
||||||
console.log("======== 6", text)
|
|
||||||
|
|
||||||
let {uploaded, url, ...task} = parseJson(text) || {}
|
let {uploaded, url, ...task} = parseJson(text) || {}
|
||||||
|
|
||||||
@@ -569,7 +556,6 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
|
|||||||
|
|
||||||
return {result}
|
return {result}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log("========= error", String(e))
|
|
||||||
console.error("Error caught when uploading file:", e)
|
console.error("Error caught when uploading file:", e)
|
||||||
|
|
||||||
return {error: e.toString()}
|
return {error: e.toString()}
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {randomId} from "@welshman/lib"
|
||||||
|
import {removeAt, insertAt} from "@welshman/lib"
|
||||||
|
import {preventDefault, stopPropagation} from "@lib/html"
|
||||||
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
|
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: (string | File)[]
|
||||||
|
multiple?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
let {value = $bindable(), multiple = true}: Props = $props()
|
||||||
|
|
||||||
|
const id = randomId()
|
||||||
|
|
||||||
|
const getImageUrl = (item: string | File): string => {
|
||||||
|
if (typeof item === "string") return item
|
||||||
|
return URL.createObjectURL(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addFiles = (files: FileList | File[]) => {
|
||||||
|
const newFiles = Array.from(files).filter(file => file.type.startsWith("image/"))
|
||||||
|
value = [...value, ...newFiles]
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeItem = (index: number) => {
|
||||||
|
value = removeAt(index, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFileChange = (e: Event) => {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
if (target.files?.length) {
|
||||||
|
addFiles(target.files)
|
||||||
|
target.value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDrop = (e: Event) => {
|
||||||
|
dropActive = false
|
||||||
|
const dragEvent = e as DragEvent
|
||||||
|
if (dragEvent.dataTransfer?.files?.length) {
|
||||||
|
addFiles(dragEvent.dataTransfer.files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDragEnter = (e: Event) => {
|
||||||
|
dropActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDragOver = (e: Event) => {
|
||||||
|
dropActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDragLeave = (e: Event) => {
|
||||||
|
dropActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let draggedIndex: number | null = $state(null)
|
||||||
|
let dropActive = $state(false)
|
||||||
|
|
||||||
|
const handleDragStart = (e: DragEvent, index: number) => {
|
||||||
|
draggedIndex = index
|
||||||
|
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.dataTransfer.effectAllowed = "move"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragOver = (e: DragEvent, index: number) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (draggedIndex !== null && draggedIndex !== index) {
|
||||||
|
value = insertAt(index, value[draggedIndex], removeAt(draggedIndex, value))
|
||||||
|
draggedIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragEnd = () => {
|
||||||
|
draggedIndex = null
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="grid grid-cols-3 gap-3" role="list">
|
||||||
|
{#each value as item, index (index)}
|
||||||
|
<div
|
||||||
|
class="relative aspect-square cursor-move"
|
||||||
|
class:border-primary={draggedIndex === index}
|
||||||
|
draggable="true"
|
||||||
|
role="listitem"
|
||||||
|
aria-label="Draggable image"
|
||||||
|
ondragstart={e => handleDragStart(e, index)}
|
||||||
|
ondragover={e => handleDragOver(e, index)}
|
||||||
|
ondragend={handleDragEnd}>
|
||||||
|
<img
|
||||||
|
src={getImageUrl(item)}
|
||||||
|
alt="Upload preview"
|
||||||
|
class="h-full w-full object-cover rounded-box" />
|
||||||
|
<Button
|
||||||
|
class="absolute right-1 top-1 w-5 h-5 flex justify-center items-center rounded-full bg-base-100"
|
||||||
|
onclick={() => removeItem(index)}>
|
||||||
|
<Icon icon={CloseCircle} size={6} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<label
|
||||||
|
for={id}
|
||||||
|
class="flex cursor-pointer aspect-square items-center justify-center rounded-lg border-2 border-dashed border-base-content text-sm"
|
||||||
|
class:border-primary={dropActive}
|
||||||
|
aria-label="Drag and drop images here or click to select"
|
||||||
|
ondragenter={stopPropagation(preventDefault(onDragEnter))}
|
||||||
|
ondragover={stopPropagation(preventDefault(onDragOver))}
|
||||||
|
ondragleave={stopPropagation(preventDefault(onDragLeave))}
|
||||||
|
ondrop={stopPropagation(preventDefault(onDrop))}>
|
||||||
|
<div class="flex flex-col items-center gap-2 text-center">
|
||||||
|
<Icon icon={GallerySend} size={8} />
|
||||||
|
<p class="text-sm opacity-70">Drag and drop images or click to select</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<input {id} type="file" accept="image/*" {multiple} onchange={onFileChange} class="hidden" />
|
||||||
|
</div>
|
||||||
@@ -12,6 +12,6 @@
|
|||||||
const {children, tag = "div", ...props}: Props = $props()
|
const {children, tag = "div", ...props}: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:element this={tag} {...props} class={cx("flex flex-col overflow-hidden pb-6", props.class)}>
|
<svelte:element this={tag} {...props} class={cx("flex flex-col overflow-hidden", props.class)}>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</svelte:element>
|
</svelte:element>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
const {children, ...props}: Props = $props()
|
const {children, ...props}: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class={cx("scroll-container overflow-y-auto min-h-0 flex flex-col gap-4 p-6", props.class)}>
|
||||||
class={cx("scroll-container overflow-y-auto min-h-0 flex flex-col gap-4 p-6 pb-0", props.class)}>
|
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import PageContent from "@lib/components/PageContent.svelte"
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import NoteContent from "@app/components/NoteContent.svelte"
|
||||||
import NoteCard from "@app/components/NoteCard.svelte"
|
import NoteCard from "@app/components/NoteCard.svelte"
|
||||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||||
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
<div class="flex py-2 opacity-50">
|
<div class="flex py-2 opacity-50">
|
||||||
<div class="h-px flex-grow bg-base-content opacity-25"></div>
|
<div class="h-px flex-grow bg-base-content opacity-25"></div>
|
||||||
</div>
|
</div>
|
||||||
<Content showEntire event={$event} {url} />
|
<NoteContent showEntire event={$event} {url} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full flex-col justify-end sm:flex-row">
|
<div class="flex w-full flex-col justify-end sm:flex-row">
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
{#each sortBy(e => e.created_at, $replies).slice(0, showAll ? undefined : 4) as reply (reply.id)}
|
{#each sortBy(e => e.created_at, $replies).slice(0, showAll ? undefined : 4) as reply (reply.id)}
|
||||||
<NoteCard event={reply} {url} class="card2 bg-alt z-feature w-full">
|
<NoteCard event={reply} {url} class="card2 bg-alt z-feature w-full">
|
||||||
<div class="col-3 ml-12">
|
<div class="col-3 ml-12">
|
||||||
<Content showEntire event={reply} {url} />
|
<NoteContent showEntire event={reply} {url} />
|
||||||
<CalendarEventActions event={reply} {url} />
|
<CalendarEventActions event={reply} {url} />
|
||||||
</div>
|
</div>
|
||||||
</NoteCard>
|
</NoteCard>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import PageContent from "@lib/components/PageContent.svelte"
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import NoteContent from "@app/components/NoteContent.svelte"
|
||||||
import NoteCard from "@app/components/NoteCard.svelte"
|
import NoteCard from "@app/components/NoteCard.svelte"
|
||||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||||
import ClassifiedActions from "@app/components/ClassifiedActions.svelte"
|
import ClassifiedActions from "@app/components/ClassifiedActions.svelte"
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
|
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
|
||||||
<div class="col-3 ml-12">
|
<div class="col-3 ml-12">
|
||||||
<Content showEntire event={$event} {url} />
|
<NoteContent showEntire event={$event} {url} />
|
||||||
<ClassifiedActions showRoom event={$event} {url} />
|
<ClassifiedActions showRoom event={$event} {url} />
|
||||||
</div>
|
</div>
|
||||||
</NoteCard>
|
</NoteCard>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
{#each $replies.slice(0, showAll ? undefined : 4) as reply (reply.id)}
|
{#each $replies.slice(0, showAll ? undefined : 4) as reply (reply.id)}
|
||||||
<NoteCard event={reply} {url} class="card2 bg-alt z-feature w-full">
|
<NoteCard event={reply} {url} class="card2 bg-alt z-feature w-full">
|
||||||
<div class="col-3 ml-12">
|
<div class="col-3 ml-12">
|
||||||
<Content showEntire event={reply} {url} />
|
<NoteContent showEntire event={reply} {url} />
|
||||||
<CommentActions segment="classifieds" event={reply} {url} />
|
<CommentActions segment="classifieds" event={reply} {url} />
|
||||||
</div>
|
</div>
|
||||||
</NoteCard>
|
</NoteCard>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import PageContent from "@lib/components/PageContent.svelte"
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import NoteContent from "@app/components/NoteContent.svelte"
|
||||||
import NoteCard from "@app/components/NoteCard.svelte"
|
import NoteCard from "@app/components/NoteCard.svelte"
|
||||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||||
import GoalSummary from "@app/components/GoalSummary.svelte"
|
import GoalSummary from "@app/components/GoalSummary.svelte"
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
|
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
|
||||||
<div class="col-3 ml-12">
|
<div class="col-3 ml-12">
|
||||||
<Content showEntire event={{...$event, content: summary}} {url} />
|
<NoteContent showEntire event={{...$event, content: summary}} {url} />
|
||||||
<GoalSummary event={$event} {url} />
|
<GoalSummary event={$event} {url} />
|
||||||
<GoalActions showRoom event={$event} {url} />
|
<GoalActions showRoom event={$event} {url} />
|
||||||
</div>
|
</div>
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
{#each $replies.slice(0, showAll ? undefined : 4) as reply (reply.id)}
|
{#each $replies.slice(0, showAll ? undefined : 4) as reply (reply.id)}
|
||||||
<NoteCard event={reply} {url} class="card2 bg-alt z-feature w-full">
|
<NoteCard event={reply} {url} class="card2 bg-alt z-feature w-full">
|
||||||
<div class="col-3 ml-12">
|
<div class="col-3 ml-12">
|
||||||
<Content showEntire event={reply} {url} />
|
<NoteContent showEntire event={reply} {url} />
|
||||||
<CommentActions segment="goals" event={reply} {url} />
|
<CommentActions segment="goals" event={reply} {url} />
|
||||||
</div>
|
</div>
|
||||||
</NoteCard>
|
</NoteCard>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import PageContent from "@lib/components/PageContent.svelte"
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import NoteContent from "@app/components/NoteContent.svelte"
|
||||||
import NoteCard from "@app/components/NoteCard.svelte"
|
import NoteCard from "@app/components/NoteCard.svelte"
|
||||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||||
import ThreadActions from "@app/components/ThreadActions.svelte"
|
import ThreadActions from "@app/components/ThreadActions.svelte"
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
|
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
|
||||||
<div class="col-3 ml-12">
|
<div class="col-3 ml-12">
|
||||||
<Content showEntire event={$event} {url} />
|
<NoteContent showEntire event={$event} {url} />
|
||||||
<ThreadActions showRoom event={$event} {url} />
|
<ThreadActions showRoom event={$event} {url} />
|
||||||
</div>
|
</div>
|
||||||
</NoteCard>
|
</NoteCard>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
{#each $replies.slice(0, showAll ? undefined : 4) as reply (reply.id)}
|
{#each $replies.slice(0, showAll ? undefined : 4) as reply (reply.id)}
|
||||||
<NoteCard event={reply} {url} class="card2 bg-alt z-feature w-full">
|
<NoteCard event={reply} {url} class="card2 bg-alt z-feature w-full">
|
||||||
<div class="col-3 ml-12">
|
<div class="col-3 ml-12">
|
||||||
<Content showEntire event={reply} {url} />
|
<NoteContent showEntire event={reply} {url} />
|
||||||
<CommentActions segment="threads" event={reply} {url} />
|
<CommentActions segment="threads" event={reply} {url} />
|
||||||
</div>
|
</div>
|
||||||
</NoteCard>
|
</NoteCard>
|
||||||
|
|||||||
Reference in New Issue
Block a user