✨ Attachments
This commit is contained in:
parent
5e4d5f77c5
commit
86783316a1
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/uploads
|
@ -15,6 +15,7 @@ func RunMigration(source *gorm.DB) error {
|
|||||||
&models.Post{},
|
&models.Post{},
|
||||||
&models.PostLike{},
|
&models.PostLike{},
|
||||||
&models.PostDislike{},
|
&models.PostDislike{},
|
||||||
|
&models.Attachment{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ type Account struct {
|
|||||||
EmailAddress string `json:"email_address"`
|
EmailAddress string `json:"email_address"`
|
||||||
PowerLevel int `json:"power_level"`
|
PowerLevel int `json:"power_level"`
|
||||||
Posts []Post `json:"posts" gorm:"foreignKey:AuthorID"`
|
Posts []Post `json:"posts" gorm:"foreignKey:AuthorID"`
|
||||||
|
Attachments []Attachment `json:"attachments" gorm:"foreignKey:AuthorID"`
|
||||||
LikedPosts []PostLike `json:"liked_posts"`
|
LikedPosts []PostLike `json:"liked_posts"`
|
||||||
DislikedPosts []PostDislike `json:"disliked_posts"`
|
DislikedPosts []PostDislike `json:"disliked_posts"`
|
||||||
Realms []Realm `json:"realms"`
|
Realms []Realm `json:"realms"`
|
||||||
|
29
pkg/models/attachments.go
Normal file
29
pkg/models/attachments.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Attachment struct {
|
||||||
|
BaseModel
|
||||||
|
|
||||||
|
FileID string `json:"file_id"`
|
||||||
|
Filesize int64 `json:"filesize"`
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
Mimetype string `json:"mimetype"`
|
||||||
|
Post *Post `json:"post"`
|
||||||
|
Author Account `json:"author"`
|
||||||
|
PostID *uint `json:"post_id"`
|
||||||
|
AuthorID uint `json:"author_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Attachment) GetStoragePath() string {
|
||||||
|
basepath := viper.GetString("content")
|
||||||
|
return filepath.Join(basepath, v.FileID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Attachment) GetAccessPath() string {
|
||||||
|
return fmt.Sprintf("/api/attachments/o/%s", v.FileID)
|
||||||
|
}
|
@ -10,6 +10,7 @@ type Post struct {
|
|||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Tags []Tag `json:"tags" gorm:"many2many:post_tags"`
|
Tags []Tag `json:"tags" gorm:"many2many:post_tags"`
|
||||||
Categories []Category `json:"categories" gorm:"many2many:post_categories"`
|
Categories []Category `json:"categories" gorm:"many2many:post_categories"`
|
||||||
|
Attachments []Attachment `json:"attachments"`
|
||||||
LikedAccounts []PostLike `json:"liked_accounts"`
|
LikedAccounts []PostLike `json:"liked_accounts"`
|
||||||
DislikedAccounts []PostDislike `json:"disliked_accounts"`
|
DislikedAccounts []PostDislike `json:"disliked_accounts"`
|
||||||
RepostTo *Post `json:"repost_to" gorm:"foreignKey:RepostID"`
|
RepostTo *Post `json:"repost_to" gorm:"foreignKey:RepostID"`
|
||||||
|
38
pkg/server/attachments_api.go
Normal file
38
pkg/server/attachments_api.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.smartsheep.studio/hydrogen/interactive/pkg/models"
|
||||||
|
"code.smartsheep.studio/hydrogen/interactive/pkg/services"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func openAttachment(c *fiber.Ctx) error {
|
||||||
|
id := c.Params("fileId")
|
||||||
|
basepath := viper.GetString("content")
|
||||||
|
|
||||||
|
return c.SendFile(filepath.Join(basepath, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadAttachment(c *fiber.Ctx) error {
|
||||||
|
user := c.Locals("principal").(models.Account)
|
||||||
|
file, err := c.FormFile("attachment")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
attachment, err := services.NewAttachment(user, file)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.SaveFile(file, attachment.GetStoragePath()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"info": attachment,
|
||||||
|
"url": attachment.GetAccessPath(),
|
||||||
|
})
|
||||||
|
}
|
@ -36,7 +36,7 @@ func doLogin(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func doPostLogin(c *fiber.Ctx) error {
|
func postLogin(c *fiber.Ctx) error {
|
||||||
buildOauth2Config()
|
buildOauth2Config()
|
||||||
code := c.Query("code")
|
code := c.Query("code")
|
||||||
|
|
||||||
|
@ -48,14 +48,15 @@ func createPost(c *fiber.Ctx) error {
|
|||||||
user := c.Locals("principal").(models.Account)
|
user := c.Locals("principal").(models.Account)
|
||||||
|
|
||||||
var data struct {
|
var data struct {
|
||||||
Alias string `json:"alias"`
|
Alias string `json:"alias"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Content string `json:"content" validate:"required"`
|
Content string `json:"content" validate:"required"`
|
||||||
Tags []models.Tag `json:"tags"`
|
Tags []models.Tag `json:"tags"`
|
||||||
Categories []models.Category `json:"categories"`
|
Categories []models.Category `json:"categories"`
|
||||||
PublishedAt *time.Time `json:"published_at"`
|
Attachments []models.Attachment `json:"attachments"`
|
||||||
RepostTo uint `json:"repost_to"`
|
PublishedAt *time.Time `json:"published_at"`
|
||||||
ReplyTo uint `json:"reply_to"`
|
RepostTo uint `json:"repost_to"`
|
||||||
|
ReplyTo uint `json:"reply_to"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := BindAndValidate(c, &data); err != nil {
|
if err := BindAndValidate(c, &data); err != nil {
|
||||||
@ -94,6 +95,7 @@ func createPost(c *fiber.Ctx) error {
|
|||||||
data.Alias,
|
data.Alias,
|
||||||
data.Title,
|
data.Title,
|
||||||
data.Content,
|
data.Content,
|
||||||
|
data.Attachments,
|
||||||
data.Categories,
|
data.Categories,
|
||||||
data.Tags,
|
data.Tags,
|
||||||
data.PublishedAt,
|
data.PublishedAt,
|
||||||
|
@ -56,14 +56,17 @@ func NewServer() {
|
|||||||
|
|
||||||
api := A.Group("/api").Name("API")
|
api := A.Group("/api").Name("API")
|
||||||
{
|
{
|
||||||
|
api.Get("/auth", doLogin)
|
||||||
|
api.Get("/auth/callback", postLogin)
|
||||||
|
api.Post("/auth/refresh", doRefreshToken)
|
||||||
|
|
||||||
api.Get("/users/me", auth, getUserinfo)
|
api.Get("/users/me", auth, getUserinfo)
|
||||||
api.Get("/users/:accountId", getOthersInfo)
|
api.Get("/users/:accountId", getOthersInfo)
|
||||||
api.Get("/users/:accountId/follow", auth, getAccountFollowed)
|
api.Get("/users/:accountId/follow", auth, getAccountFollowed)
|
||||||
api.Post("/users/:accountId/follow", auth, doFollowAccount)
|
api.Post("/users/:accountId/follow", auth, doFollowAccount)
|
||||||
|
|
||||||
api.Get("/auth", doLogin)
|
api.Get("/attachments/o/:fileId", openAttachment)
|
||||||
api.Get("/auth/callback", doPostLogin)
|
api.Post("/attachments", auth, uploadAttachment)
|
||||||
api.Post("/auth/refresh", doRefreshToken)
|
|
||||||
|
|
||||||
api.Get("/posts", listPost)
|
api.Get("/posts", listPost)
|
||||||
api.Post("/posts", auth, createPost)
|
api.Post("/posts", auth, createPost)
|
||||||
|
40
pkg/services/attachments.go
Normal file
40
pkg/services/attachments.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.smartsheep.studio/hydrogen/interactive/pkg/database"
|
||||||
|
"code.smartsheep.studio/hydrogen/interactive/pkg/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewAttachment(user models.Account, header *multipart.FileHeader) (models.Attachment, error) {
|
||||||
|
attachment := models.Attachment{
|
||||||
|
FileID: uuid.NewString(),
|
||||||
|
Filesize: header.Size,
|
||||||
|
Filename: header.Filename,
|
||||||
|
Mimetype: "unknown/unknown",
|
||||||
|
PostID: nil,
|
||||||
|
AuthorID: user.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open file
|
||||||
|
file, err := header.Open()
|
||||||
|
if err != nil {
|
||||||
|
return attachment, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Detect mimetype
|
||||||
|
fileHeader := make([]byte, 512)
|
||||||
|
_, err = file.Read(fileHeader)
|
||||||
|
if err != nil {
|
||||||
|
return attachment, err
|
||||||
|
}
|
||||||
|
attachment.Mimetype = http.DetectContentType(fileHeader)
|
||||||
|
|
||||||
|
// Save into database
|
||||||
|
err = database.C.Save(&attachment).Error
|
||||||
|
|
||||||
|
return attachment, err
|
||||||
|
}
|
@ -18,10 +18,13 @@ func ListPost(tx *gorm.DB, take int, offset int) ([]*models.Post, error) {
|
|||||||
Limit(take).
|
Limit(take).
|
||||||
Offset(offset).
|
Offset(offset).
|
||||||
Preload("Author").
|
Preload("Author").
|
||||||
|
Preload("Attachments").
|
||||||
Preload("RepostTo").
|
Preload("RepostTo").
|
||||||
Preload("ReplyTo").
|
Preload("ReplyTo").
|
||||||
Preload("RepostTo.Author").
|
Preload("RepostTo.Author").
|
||||||
Preload("ReplyTo.Author").
|
Preload("ReplyTo.Author").
|
||||||
|
Preload("RepostTo.Attachments").
|
||||||
|
Preload("ReplyTo.Attachments").
|
||||||
Find(&posts).Error; err != nil {
|
Find(&posts).Error; err != nil {
|
||||||
return posts, err
|
return posts, err
|
||||||
}
|
}
|
||||||
@ -66,6 +69,7 @@ WHERE t.id IN (?)`, prefix, prefix, prefix), postIds).Scan(&reactInfo)
|
|||||||
func NewPost(
|
func NewPost(
|
||||||
user models.Account,
|
user models.Account,
|
||||||
alias, title, content string,
|
alias, title, content string,
|
||||||
|
attachments []models.Attachment,
|
||||||
categories []models.Category,
|
categories []models.Category,
|
||||||
tags []models.Tag,
|
tags []models.Tag,
|
||||||
publishedAt *time.Time,
|
publishedAt *time.Time,
|
||||||
@ -77,6 +81,7 @@ func NewPost(
|
|||||||
alias,
|
alias,
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
|
attachments,
|
||||||
categories,
|
categories,
|
||||||
tags,
|
tags,
|
||||||
publishedAt,
|
publishedAt,
|
||||||
@ -89,6 +94,7 @@ func NewPostWithRealm(
|
|||||||
user models.Account,
|
user models.Account,
|
||||||
realm *models.Realm,
|
realm *models.Realm,
|
||||||
alias, title, content string,
|
alias, title, content string,
|
||||||
|
attachments []models.Attachment,
|
||||||
categories []models.Category,
|
categories []models.Category,
|
||||||
tags []models.Tag,
|
tags []models.Tag,
|
||||||
publishedAt *time.Time,
|
publishedAt *time.Time,
|
||||||
@ -122,6 +128,7 @@ func NewPostWithRealm(
|
|||||||
Alias: alias,
|
Alias: alias,
|
||||||
Title: title,
|
Title: title,
|
||||||
Content: content,
|
Content: content,
|
||||||
|
Attachments: attachments,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
Categories: categories,
|
Categories: categories,
|
||||||
AuthorID: user.ID,
|
AuthorID: user.ID,
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||||
"@solidjs/router": "^0.10.10",
|
"@solidjs/router": "^0.10.10",
|
||||||
|
"medium-zoom": "^1.1.0",
|
||||||
"solid-js": "^1.8.7",
|
"solid-js": "^1.8.7",
|
||||||
"universal-cookie": "^7.0.2"
|
"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 { createSignal, Show } from "solid-js";
|
||||||
import { getAtk, useUserinfo } from "../stores/userinfo.tsx";
|
import { getAtk, useUserinfo } from "../stores/userinfo.tsx";
|
||||||
|
import PostAttachments from "./PostAttachments.tsx";
|
||||||
|
|
||||||
export default function PostItem(props: {
|
export default function PostItem(props: {
|
||||||
post: any,
|
post: any,
|
||||||
@ -58,6 +59,8 @@ export default function PostItem(props: {
|
|||||||
<h2 class="card-title">{props.post.title}</h2>
|
<h2 class="card-title">{props.post.title}</h2>
|
||||||
<article class="prose">{props.post.content}</article>
|
<article class="prose">{props.post.content}</article>
|
||||||
|
|
||||||
|
<PostAttachments attachments={props.post.attachments ?? []} />
|
||||||
|
|
||||||
<Show when={props.post.repost_to}>
|
<Show when={props.post.repost_to}>
|
||||||
<p class="text-xs mt-3 mb-2">
|
<p class="text-xs mt-3 mb-2">
|
||||||
<i class="fa-solid fa-retweet me-2"></i>
|
<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 { getAtk, useUserinfo } from "../stores/userinfo.tsx";
|
||||||
|
|
||||||
import styles from "./PostPublish.module.css";
|
import styles from "./PostPublish.module.css";
|
||||||
@ -15,6 +15,11 @@ export default function PostPublish(props: {
|
|||||||
const userinfo = useUserinfo();
|
const userinfo = useUserinfo();
|
||||||
|
|
||||||
const [submitting, setSubmitting] = createSignal(false);
|
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) {
|
async function doPost(evt: SubmitEvent) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
@ -34,6 +39,7 @@ export default function PostPublish(props: {
|
|||||||
alias: data.alias ?? crypto.randomUUID().replace(/-/g, ""),
|
alias: data.alias ?? crypto.randomUUID().replace(/-/g, ""),
|
||||||
title: data.title,
|
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(),
|
published_at: data.published_at ? new Date(data.published_at as string) : new Date(),
|
||||||
repost_to: props.reposting?.id,
|
repost_to: props.reposting?.id,
|
||||||
reply_to: props.replying?.id
|
reply_to: props.replying?.id
|
||||||
@ -55,6 +61,7 @@ export default function PostPublish(props: {
|
|||||||
const form = evt.target as HTMLFormElement;
|
const form = evt.target as HTMLFormElement;
|
||||||
const data = Object.fromEntries(new FormData(form));
|
const data = Object.fromEntries(new FormData(form));
|
||||||
if (!data.content) return;
|
if (!data.content) return;
|
||||||
|
if (uploading()) return;
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const res = await fetch(`/api/posts/${props.editing?.id}`, {
|
const res = await fetch(`/api/posts/${props.editing?.id}`, {
|
||||||
@ -66,7 +73,9 @@ export default function PostPublish(props: {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
alias: data.alias ?? crypto.randomUUID().replace(/-/g, ""),
|
alias: data.alias ?? crypto.randomUUID().replace(/-/g, ""),
|
||||||
title: data.title,
|
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) {
|
if (res.status !== 200) {
|
||||||
@ -79,103 +88,171 @@ export default function PostPublish(props: {
|
|||||||
setSubmitting(false);
|
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 (
|
return (
|
||||||
<form id="publish" onSubmit={props.editing ? doEdit : doPost} onReset={props.onReset}>
|
<>
|
||||||
<div id="publish-identity" class="flex border-y border-base-200">
|
<form id="publish" onSubmit={props.editing ? doEdit : doPost} onReset={() => resetForm()}>
|
||||||
<div class="avatar pl-[20px]">
|
<div id="publish-identity" class="flex border-y border-base-200">
|
||||||
<div class="w-12">
|
<div class="avatar pl-[20px]">
|
||||||
<Show when={userinfo?.profiles?.avatar}
|
<div class="w-12">
|
||||||
fallback={<span class="text-3xl">{userinfo?.displayName.substring(0, 1)}</span>}>
|
<Show when={userinfo?.profiles?.avatar}
|
||||||
<img alt="avatar" src={userinfo?.profiles?.avatar} />
|
fallback={<span class="text-3xl">{userinfo?.displayName.substring(0, 1)}</span>}>
|
||||||
</Show>
|
<img alt="avatar" src={userinfo?.profiles?.avatar} />
|
||||||
</div>
|
</Show>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
<input name="published_at" type="datetime-local" placeholder="Pick a date"
|
</div>
|
||||||
class="input input-bordered w-full" />
|
<div class="flex flex-grow">
|
||||||
<div class="label">
|
<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">
|
<span class="label-text-alt">
|
||||||
Before this time, your post will not be visible for everyone.
|
Before this time, your post will not be visible for everyone.
|
||||||
You can modify this plan on Creator Hub.
|
You can modify this plan on Creator Hub.
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div class="modal-action">
|
||||||
|
<button type="button" class="btn" onClick={() => closeModel("#planning-publish")}>Close</button>
|
||||||
</div>
|
</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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</form>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -5,4 +5,12 @@
|
|||||||
html, body {
|
html, body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium-zoom-image--opened {
|
||||||
|
z-index: 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium-zoom-overlay {
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user