Attachments

This commit is contained in:
2024-02-04 18:40:20 +08:00
parent 5e4d5f77c5
commit 86783316a1
17 changed files with 396 additions and 101 deletions

View File

@ -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"
},

View File

@ -0,0 +1,3 @@
.attachmentsControl {
background-color: transparent !important;
}

View 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>
</>
);
}

View File

@ -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>

View File

@ -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>
</>
);
}

View File

@ -5,4 +5,12 @@
html, body {
padding: 0;
margin: 0;
}
.medium-zoom-image--opened {
z-index: 15;
}
.medium-zoom-overlay {
z-index: 10;
}