✨ Attachments
This commit is contained in:
@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@solidjs/router": "^0.10.10",
|
||||
"medium-zoom": "^1.1.0",
|
||||
"solid-js": "^1.8.7",
|
||||
"universal-cookie": "^7.0.2"
|
||||
},
|
||||
|
3
pkg/view/src/components/PostAttachments.module.css
Normal file
3
pkg/view/src/components/PostAttachments.module.css
Normal file
@ -0,0 +1,3 @@
|
||||
.attachmentsControl {
|
||||
background-color: transparent !important;
|
||||
}
|
80
pkg/view/src/components/PostAttachments.tsx
Normal file
80
pkg/view/src/components/PostAttachments.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { createEffect, createMemo, createSignal, Match, Switch } from "solid-js";
|
||||
import mediumZoom from "medium-zoom";
|
||||
|
||||
import styles from "./PostAttachments.module.css";
|
||||
|
||||
export default function PostAttachments(props: { attachments: any[] }) {
|
||||
if (props.attachments.length <= 0) return null;
|
||||
|
||||
const [focus, setFocus] = createSignal(0);
|
||||
const item = createMemo(() => props.attachments[focus()]);
|
||||
|
||||
function getRenderType(item: any): string {
|
||||
return item.mimetype.split("/")[0];
|
||||
}
|
||||
|
||||
function getUrl(item: any): string {
|
||||
return `/api/attachments/o/${item.file_id}`;
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
mediumZoom(document.querySelectorAll(".attachment-image img"), {
|
||||
background: "var(--fallback-b1,oklch(var(--b1)/1))"
|
||||
});
|
||||
}, [focus()]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p class="text-xs mt-3 mb-2">
|
||||
<i class="fa-solid fa-paperclip me-2"></i>
|
||||
Attached {props.attachments.length} file{props.attachments.length > 1 ? "s" : null}
|
||||
</p>
|
||||
<div class="border border-base-200">
|
||||
<Switch fallback={
|
||||
<div class="py-16 flex justify-center items-center">
|
||||
<div class="text-center">
|
||||
<i class="fa-solid fa-circle-question text-3xl"></i>
|
||||
<p class="mt-3">{item().filename}</p>
|
||||
|
||||
<div class="flex gap-3 w-full">
|
||||
<p class="text-sm">{item().filesize} Bytes</p>
|
||||
<p class="text-sm">{item().mimetype}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<a class="link" href={getUrl(item())} target="_blank">Open in browser</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<Match when={getRenderType(item()) === "image"}>
|
||||
<figure class="attachment-image">
|
||||
<img class="object-cover" src={getUrl(item())} alt={item().filename} />
|
||||
</figure>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
<div id="attachments-control" class="flex justify-between border-t border-base-200">
|
||||
<div class="flex">
|
||||
<button class={`w-12 h-12 btn btn-ghost ${styles.attachmentsControl}`}
|
||||
disabled={focus() - 1 < 0}
|
||||
onClick={() => setFocus(focus() - 1)}>
|
||||
<i class="fa-solid fa-caret-left"></i>
|
||||
</button>
|
||||
<button class={`w-12 h-12 btn btn-ghost ${styles.attachmentsControl}`}
|
||||
disabled={focus() + 1 >= props.attachments.length}
|
||||
onClick={() => setFocus(focus() + 1)}>
|
||||
<i class="fa-solid fa-caret-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="h-12 px-5 py-3.5 text-sm">
|
||||
File {focus() + 1}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { getAtk, useUserinfo } from "../stores/userinfo.tsx";
|
||||
import PostAttachments from "./PostAttachments.tsx";
|
||||
|
||||
export default function PostItem(props: {
|
||||
post: any,
|
||||
@ -58,6 +59,8 @@ export default function PostItem(props: {
|
||||
<h2 class="card-title">{props.post.title}</h2>
|
||||
<article class="prose">{props.post.content}</article>
|
||||
|
||||
<PostAttachments attachments={props.post.attachments ?? []} />
|
||||
|
||||
<Show when={props.post.repost_to}>
|
||||
<p class="text-xs mt-3 mb-2">
|
||||
<i class="fa-solid fa-retweet me-2"></i>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { createEffect, createSignal, For, Show } from "solid-js";
|
||||
import { getAtk, useUserinfo } from "../stores/userinfo.tsx";
|
||||
|
||||
import styles from "./PostPublish.module.css";
|
||||
@ -15,6 +15,11 @@ export default function PostPublish(props: {
|
||||
const userinfo = useUserinfo();
|
||||
|
||||
const [submitting, setSubmitting] = createSignal(false);
|
||||
const [uploading, setUploading] = createSignal(false);
|
||||
|
||||
const [attachments, setAttachments] = createSignal<any[]>([]);
|
||||
|
||||
createEffect(() => setAttachments(props.editing?.attachments ?? []), [props.editing]);
|
||||
|
||||
async function doPost(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
@ -34,6 +39,7 @@ export default function PostPublish(props: {
|
||||
alias: data.alias ?? crypto.randomUUID().replace(/-/g, ""),
|
||||
title: data.title,
|
||||
content: data.content,
|
||||
attachments: attachments(),
|
||||
published_at: data.published_at ? new Date(data.published_at as string) : new Date(),
|
||||
repost_to: props.reposting?.id,
|
||||
reply_to: props.replying?.id
|
||||
@ -55,6 +61,7 @@ export default function PostPublish(props: {
|
||||
const form = evt.target as HTMLFormElement;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
if (!data.content) return;
|
||||
if (uploading()) return;
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await fetch(`/api/posts/${props.editing?.id}`, {
|
||||
@ -66,7 +73,9 @@ export default function PostPublish(props: {
|
||||
body: JSON.stringify({
|
||||
alias: data.alias ?? crypto.randomUUID().replace(/-/g, ""),
|
||||
title: data.title,
|
||||
content: data.content
|
||||
content: data.content,
|
||||
attachments: attachments(),
|
||||
published_at: data.published_at ? new Date(data.published_at as string) : new Date()
|
||||
})
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
@ -79,103 +88,171 @@ export default function PostPublish(props: {
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
async function uploadAttachments(evt: SubmitEvent) {
|
||||
evt.preventDefault();
|
||||
|
||||
const data = new FormData(evt.target as HTMLFormElement);
|
||||
if (!data.get("attachment")) return;
|
||||
|
||||
setUploading(true);
|
||||
const res = await fetch("/api/attachments", {
|
||||
method: "POST",
|
||||
headers: { "Authorization": `Bearer ${getAtk()}` },
|
||||
body: data
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
props.onError(await res.text());
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setAttachments(attachments().concat([data.info]));
|
||||
props.onError(null);
|
||||
}
|
||||
setUploading(false);
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
setAttachments([]);
|
||||
props.onReset();
|
||||
}
|
||||
|
||||
return (
|
||||
<form id="publish" onSubmit={props.editing ? doEdit : doPost} onReset={props.onReset}>
|
||||
<div id="publish-identity" class="flex border-y border-base-200">
|
||||
<div class="avatar pl-[20px]">
|
||||
<div class="w-12">
|
||||
<Show when={userinfo?.profiles?.avatar}
|
||||
fallback={<span class="text-3xl">{userinfo?.displayName.substring(0, 1)}</span>}>
|
||||
<img alt="avatar" src={userinfo?.profiles?.avatar} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-grow">
|
||||
<input name="title" value={props.editing?.title ?? ""}
|
||||
class={`${styles.publishInput} input w-full`}
|
||||
placeholder="The describe for a long content (Optional)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={props.reposting}>
|
||||
<div role="alert" class="bg-base-200 flex justify-between">
|
||||
<div class="px-5 py-3">
|
||||
<i class="fa-solid fa-circle-info me-3"></i>
|
||||
You are reposting a post from <b>{props.reposting?.author?.name}</b>
|
||||
</div>
|
||||
<button type="reset" class="btn btn-ghost w-12" disabled={submitting()}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.replying}>
|
||||
<div role="alert" class="bg-base-200 flex justify-between">
|
||||
<div class="px-5 py-3">
|
||||
<i class="fa-solid fa-circle-info me-3"></i>
|
||||
You are replying a post from <b>{props.replying?.author?.name}</b>
|
||||
</div>
|
||||
<button type="reset" class="btn btn-ghost w-12" disabled={submitting()}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.editing}>
|
||||
<div role="alert" class="bg-base-200 flex justify-between">
|
||||
<div class="px-5 py-3">
|
||||
<i class="fa-solid fa-circle-info me-3"></i>
|
||||
You are editing a post published at{" "}
|
||||
<b>{new Date(props.editing?.created_at).toLocaleString()}</b>
|
||||
</div>
|
||||
<button type="reset" class="btn btn-ghost w-12" disabled={submitting()}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<textarea name="content" value={props.editing?.content ?? ""}
|
||||
class={`${styles.publishInput} textarea w-full`}
|
||||
placeholder="What's happend?!" />
|
||||
|
||||
<div id="publish-actions" class="flex justify-between border-y border-base-200">
|
||||
<div class="flex">
|
||||
<button type="button" class="btn btn-ghost w-12">
|
||||
<i class="fa-solid fa-paperclip"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#planning-publish")}>
|
||||
<i class="fa-solid fa-calendar-day"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary" disabled={submitting()}>
|
||||
<Show when={submitting()} fallback={props.editing ? "Save changes" : "Post a post"}>
|
||||
<span class="loading"></span>
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="planning-publish" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mx-1">Planning Publish</h3>
|
||||
<label class="form-control w-full mt-3">
|
||||
<div class="label">
|
||||
<span class="label-text">Published At</span>
|
||||
<>
|
||||
<form id="publish" onSubmit={props.editing ? doEdit : doPost} onReset={() => resetForm()}>
|
||||
<div id="publish-identity" class="flex border-y border-base-200">
|
||||
<div class="avatar pl-[20px]">
|
||||
<div class="w-12">
|
||||
<Show when={userinfo?.profiles?.avatar}
|
||||
fallback={<span class="text-3xl">{userinfo?.displayName.substring(0, 1)}</span>}>
|
||||
<img alt="avatar" src={userinfo?.profiles?.avatar} />
|
||||
</Show>
|
||||
</div>
|
||||
<input name="published_at" type="datetime-local" placeholder="Pick a date"
|
||||
class="input input-bordered w-full" />
|
||||
<div class="label">
|
||||
</div>
|
||||
<div class="flex flex-grow">
|
||||
<input name="title" value={props.editing?.title ?? ""}
|
||||
class={`${styles.publishInput} input w-full`}
|
||||
placeholder="The describe for a long content (Optional)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={props.reposting}>
|
||||
<div role="alert" class="bg-base-200 flex justify-between">
|
||||
<div class="px-5 py-3">
|
||||
<i class="fa-solid fa-circle-info me-3"></i>
|
||||
You are reposting a post from <b>{props.reposting?.author?.name}</b>
|
||||
</div>
|
||||
<button type="reset" class="btn btn-ghost w-12" disabled={submitting()}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.replying}>
|
||||
<div role="alert" class="bg-base-200 flex justify-between">
|
||||
<div class="px-5 py-3">
|
||||
<i class="fa-solid fa-circle-info me-3"></i>
|
||||
You are replying a post from <b>{props.replying?.author?.name}</b>
|
||||
</div>
|
||||
<button type="reset" class="btn btn-ghost w-12" disabled={submitting()}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.editing}>
|
||||
<div role="alert" class="bg-base-200 flex justify-between">
|
||||
<div class="px-5 py-3">
|
||||
<i class="fa-solid fa-circle-info me-3"></i>
|
||||
You are editing a post published at{" "}
|
||||
<b>{new Date(props.editing?.created_at).toLocaleString()}</b>
|
||||
</div>
|
||||
<button type="reset" class="btn btn-ghost w-12" disabled={submitting()}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<textarea name="content" value={props.editing?.content ?? ""}
|
||||
class={`${styles.publishInput} textarea w-full`}
|
||||
placeholder="What's happend?!" />
|
||||
|
||||
<div id="publish-actions" class="flex justify-between border-y border-base-200">
|
||||
<div class="flex">
|
||||
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#attachments")}>
|
||||
<i class="fa-solid fa-paperclip"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#planning-publish")}>
|
||||
<i class="fa-solid fa-calendar-day"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary" disabled={submitting()}>
|
||||
<Show when={submitting()} fallback={props.editing ? "Save changes" : "Post a post"}>
|
||||
<span class="loading"></span>
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="planning-publish" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mx-1">Planning Publish</h3>
|
||||
<label class="form-control w-full mt-3">
|
||||
<div class="label">
|
||||
<span class="label-text">Published At</span>
|
||||
</div>
|
||||
<input name="published_at" type="datetime-local" placeholder="Pick a date"
|
||||
class="input input-bordered w-full" />
|
||||
<div class="label">
|
||||
<span class="label-text-alt">
|
||||
Before this time, your post will not be visible for everyone.
|
||||
You can modify this plan on Creator Hub.
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onClick={() => closeModel("#planning-publish")}>Close</button>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</dialog>
|
||||
</form>
|
||||
|
||||
|
||||
<dialog id="attachments" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mx-1">Attachments</h3>
|
||||
<form class="w-full mt-3" onSubmit={uploadAttachments}>
|
||||
<label class="form-control">
|
||||
<div class="label">
|
||||
<span class="label-text">Pick a file</span>
|
||||
</div>
|
||||
<div class="join">
|
||||
<input required type="file" name="attachment" class="join-item file-input file-input-bordered w-full" />
|
||||
<button type="submit" class="join-item btn btn-primary" disabled={uploading()}>
|
||||
<i class="fa-solid fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Click upload to add this file into list</span>
|
||||
</div>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<Show when={attachments().length > 0}>
|
||||
<h3 class="font-bold mt-3 mx-1">Attachment list</h3>
|
||||
<ol class="mt-2 mx-1 text-sm">
|
||||
<For each={attachments()}>
|
||||
{item => <li>
|
||||
<i class="fa-regular fa-file me-2"></i>
|
||||
{item.filename}
|
||||
</li>}
|
||||
</For>
|
||||
</ol>
|
||||
</Show>
|
||||
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onClick={() => closeModel("#planning-publish")}>Close</button>
|
||||
<button type="button" class="btn" onClick={() => closeModel("#attachments")}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
@ -5,4 +5,12 @@
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.medium-zoom-image--opened {
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.medium-zoom-overlay {
|
||||
z-index: 10;
|
||||
}
|
Reference in New Issue
Block a user