Post with categories and tags

This commit is contained in:
LittleSheep 2024-02-04 20:13:06 +08:00
parent 86783316a1
commit 28b4f11ccf
5 changed files with 161 additions and 8 deletions

View File

@ -3,7 +3,7 @@ package models
type Tag struct { type Tag struct {
BaseModel BaseModel
Alias string `json:"alias" gorm:"uniqueIndex"` Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Posts []Post `json:"posts" gorm:"many2many:post_tags"` Posts []Post `json:"posts" gorm:"many2many:post_tags"`
@ -12,7 +12,7 @@ type Tag struct {
type Category struct { type Category struct {
BaseModel BaseModel
Alias string `json:"alias" gorm:"uniqueIndex"` Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Posts []Post `json:"categories" gorm:"many2many:post_categories"` Posts []Post `json:"categories" gorm:"many2many:post_categories"`

View File

@ -19,12 +19,18 @@ func ListPost(tx *gorm.DB, take int, offset int) ([]*models.Post, error) {
Offset(offset). Offset(offset).
Preload("Author"). Preload("Author").
Preload("Attachments"). Preload("Attachments").
Preload("Categories").
Preload("Tags").
Preload("RepostTo"). Preload("RepostTo").
Preload("ReplyTo"). Preload("ReplyTo").
Preload("RepostTo.Author"). Preload("RepostTo.Author").
Preload("ReplyTo.Author"). Preload("ReplyTo.Author").
Preload("RepostTo.Attachments"). Preload("RepostTo.Attachments").
Preload("ReplyTo.Attachments"). Preload("ReplyTo.Attachments").
Preload("RepostTo.Categories").
Preload("ReplyTo.Categories").
Preload("RepostTo.Tags").
Preload("ReplyTo.Tags").
Find(&posts).Error; err != nil { Find(&posts).Error; err != nil {
return posts, err return posts, err
} }

View File

@ -1,4 +1,4 @@
import { createSignal, Show } from "solid-js"; import { createSignal, For, Show } from "solid-js";
import { getAtk, useUserinfo } from "../stores/userinfo.tsx"; import { getAtk, useUserinfo } from "../stores/userinfo.tsx";
import PostAttachments from "./PostAttachments.tsx"; import PostAttachments from "./PostAttachments.tsx";
@ -59,6 +59,19 @@ 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>
<div class="mt-2 flex gap-2">
<For each={props.post.categories}>
{item => <a href={`/categories/${item.alias}`} class="link link-primary">
#{item.name}
</a>}
</For>
<For each={props.post.tags}>
{item => <a href={`/tags/${item.alias}`} class="link link-primary">
#{item.name}
</a>}
</For>
</div>
<PostAttachments attachments={props.post.attachments ?? []} /> <PostAttachments attachments={props.post.attachments ?? []} />
<Show when={props.post.repost_to}> <Show when={props.post.repost_to}>

View File

@ -1,4 +1,8 @@
.publishInput { .publishInput {
outline-style: none !important; outline-style: none !important;
outline-width: 0 !important; outline-width: 0 !important;
}
.description {
color: var(--fallback-bc, oklch(var(--bc)/.8));
} }

View File

@ -18,8 +18,14 @@ export default function PostPublish(props: {
const [uploading, setUploading] = createSignal(false); const [uploading, setUploading] = createSignal(false);
const [attachments, setAttachments] = createSignal<any[]>([]); const [attachments, setAttachments] = createSignal<any[]>([]);
const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>([]);
const [tags, setTags] = createSignal<{ alias: string, name: string }[]>([]);
createEffect(() => setAttachments(props.editing?.attachments ?? []), [props.editing]); createEffect(() => {
setAttachments(props.editing?.attachments ?? []);
setCategories(props.editing?.categories ?? []);
setTags(props.editing?.tags ?? []);
}, [props.editing]);
async function doPost(evt: SubmitEvent) { async function doPost(evt: SubmitEvent) {
evt.preventDefault(); evt.preventDefault();
@ -40,6 +46,8 @@ export default function PostPublish(props: {
title: data.title, title: data.title,
content: data.content, content: data.content,
attachments: attachments(), attachments: attachments(),
categories: categories(),
tags: tags(),
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
@ -75,6 +83,8 @@ export default function PostPublish(props: {
title: data.title, title: data.title,
content: data.content, content: data.content,
attachments: attachments(), attachments: attachments(),
categories: categories(),
tags: tags(),
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()
}) })
}); });
@ -91,7 +101,8 @@ export default function PostPublish(props: {
async function uploadAttachments(evt: SubmitEvent) { async function uploadAttachments(evt: SubmitEvent) {
evt.preventDefault(); evt.preventDefault();
const data = new FormData(evt.target as HTMLFormElement); const form = evt.target as HTMLFormElement;
const data = new FormData(form);
if (!data.get("attachment")) return; if (!data.get("attachment")) return;
setUploading(true); setUploading(true);
@ -106,12 +117,47 @@ export default function PostPublish(props: {
const data = await res.json(); const data = await res.json();
setAttachments(attachments().concat([data.info])); setAttachments(attachments().concat([data.info]));
props.onError(null); props.onError(null);
form.reset();
} }
setUploading(false); setUploading(false);
} }
function addCategory(evt: SubmitEvent) {
evt.preventDefault();
const form = evt.target as HTMLFormElement;
const data = Object.fromEntries(new FormData(form));
if (!data.alias) data.alias = crypto.randomUUID().replace(/-/g, "");
if (!data.name) return;
setCategories(categories().concat([data as any]));
form.reset();
}
function removeCategory(target: any) {
setCategories(categories().filter(item => item.alias !== target.alias))
}
function addTag(evt: SubmitEvent) {
evt.preventDefault();
const form = evt.target as HTMLFormElement;
const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement));
if (!data.alias) data.alias = crypto.randomUUID().replace(/-/g, "");
if (!data.name) return;
setTags(tags().concat([data as any]));
form.reset();
}
function removeTag(target: any) {
setTags(tags().filter(item => item.alias !== target.alias))
}
function resetForm() { function resetForm() {
setAttachments([]); setAttachments([]);
setCategories([]);
setTags([]);
props.onReset(); props.onReset();
} }
@ -174,13 +220,16 @@ export default function PostPublish(props: {
placeholder="What's happend?!" /> placeholder="What's happend?!" />
<div id="publish-actions" class="flex justify-between border-y border-base-200"> <div id="publish-actions" class="flex justify-between border-y border-base-200">
<div class="flex"> <div class="flex pl-[20px]">
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#attachments")}> <button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#attachments")}>
<i class="fa-solid fa-paperclip"></i> <i class="fa-solid fa-paperclip"></i>
</button> </button>
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#planning-publish")}> <button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#planning-publish")}>
<i class="fa-solid fa-calendar-day"></i> <i class="fa-solid fa-calendar-day"></i>
</button> </button>
<button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#categories-and-tags")}>
<i class="fa-solid fa-tag"></i>
</button>
</div> </div>
<div> <div>
@ -215,7 +264,6 @@ export default function PostPublish(props: {
</dialog> </dialog>
</form> </form>
<dialog id="attachments" class="modal"> <dialog id="attachments" class="modal">
<div class="modal-box"> <div class="modal-box">
<h3 class="font-bold text-lg mx-1">Attachments</h3> <h3 class="font-bold text-lg mx-1">Attachments</h3>
@ -225,7 +273,8 @@ export default function PostPublish(props: {
<span class="label-text">Pick a file</span> <span class="label-text">Pick a file</span>
</div> </div>
<div class="join"> <div class="join">
<input required type="file" name="attachment" class="join-item file-input file-input-bordered w-full" /> <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()}> <button type="submit" class="join-item btn btn-primary" disabled={uploading()}>
<i class="fa-solid fa-upload"></i> <i class="fa-solid fa-upload"></i>
</button> </button>
@ -253,6 +302,87 @@ export default function PostPublish(props: {
</div> </div>
</div> </div>
</dialog> </dialog>
<dialog id="categories-and-tags" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg mx-1">Categories & Tags</h3>
<form class="w-full mt-3" onSubmit={addCategory}>
<label class="form-control">
<div class="label">
<span class="label-text">Add a category</span>
</div>
<div class="join">
<input type="text" name="alias" placeholder="Alias" class="join-item input input-bordered w-full" />
<input type="text" name="name" placeholder="Name" class="join-item input input-bordered w-full" />
<button type="submit" class="join-item btn btn-primary">
<i class="fa-solid fa-plus"></i>
</button>
</div>
<div class="label">
<span class="label-text-alt">
Alias is the url key of this category. Lowercase only, required length 4-24.
Leave blank for auto generate.
</span>
</div>
</label>
</form>
<Show when={categories().length > 0}>
<h3 class="font-bold mt-3 mx-1">Category list</h3>
<ol class="mt-2 mx-1 text-sm">
<For each={categories()}>
{item => <li>
<i class="fa-solid fa-layer-group me-2"></i>
{item.name} <span class={styles.description}>#{item.alias}</span>
<button class="ml-2" onClick={() => removeCategory(item)}>
<i class="fa-solid fa-delete-left"></i>
</button>
</li>}
</For>
</ol>
</Show>
<form class="w-full mt-3" onSubmit={addTag}>
<label class="form-control">
<div class="label">
<span class="label-text">Add a tag</span>
</div>
<div class="join">
<input type="text" name="alias" placeholder="Alias" class="join-item input input-bordered w-full" />
<input type="text" name="name" placeholder="Name" class="join-item input input-bordered w-full" />
<button type="submit" class="join-item btn btn-primary">
<i class="fa-solid fa-plus"></i>
</button>
</div>
<div class="label">
<span class="label-text-alt">
Alias is the url key of this tag. Lowercase only, required length 4-24.
Leave blank for auto generate.
</span>
</div>
</label>
</form>
<Show when={tags().length > 0}>
<h3 class="font-bold mt-3 mx-1">Category list</h3>
<ol class="mt-2 mx-1 text-sm">
<For each={tags()}>
{item => <li>
<i class="fa-solid fa-tag me-2"></i>
{item.name} <span class={styles.description}>#{item.alias}</span>
<button class="ml-2" onClick={() => removeTag(item)}>
<i class="fa-solid fa-delete-left"></i>
</button>
</li>}
</For>
</ol>
</Show>
<div class="modal-action">
<button type="button" class="btn" onClick={() => closeModel("#categories-and-tags")}>Close</button>
</div>
</div>
</dialog>
</> </>
); );
} }