✨ Searchable tags and categories
This commit is contained in:
parent
a619f4ca23
commit
fb2d2a3b84
@ -53,7 +53,6 @@ func getPost(c *fiber.Ctx) error {
|
|||||||
func listPost(c *fiber.Ctx) error {
|
func listPost(c *fiber.Ctx) error {
|
||||||
take := c.QueryInt("take", 0)
|
take := c.QueryInt("take", 0)
|
||||||
offset := c.QueryInt("offset", 0)
|
offset := c.QueryInt("offset", 0)
|
||||||
authorId := c.Query("authorId")
|
|
||||||
|
|
||||||
tx := database.C.
|
tx := database.C.
|
||||||
Where("realm_id IS NULL").
|
Where("realm_id IS NULL").
|
||||||
@ -61,13 +60,21 @@ func listPost(c *fiber.Ctx) error {
|
|||||||
Order("created_at desc")
|
Order("created_at desc")
|
||||||
|
|
||||||
var author models.Account
|
var author models.Account
|
||||||
if len(authorId) > 0 {
|
if len(c.Query("authorId")) > 0 {
|
||||||
if err := database.C.Where(&models.Account{Name: authorId}).First(&author).Error; err != nil {
|
if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil {
|
||||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||||
}
|
}
|
||||||
tx = tx.Where(&models.Post{AuthorID: author.ID})
|
tx = tx.Where(&models.Post{AuthorID: author.ID})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(c.Query("category")) > 0 {
|
||||||
|
tx = services.FilterPostWithCategory(tx, c.Query("category"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Query("tag")) > 0 {
|
||||||
|
tx = services.FilterPostWithTag(tx, c.Query("tag"))
|
||||||
|
}
|
||||||
|
|
||||||
var count int64
|
var count int64
|
||||||
if err := tx.
|
if err := tx.
|
||||||
Model(&models.Post{}).
|
Model(&models.Post{}).
|
||||||
|
@ -30,6 +30,20 @@ func PreloadRelatedPost(tx *gorm.DB) *gorm.DB {
|
|||||||
Preload("ReplyTo.Tags")
|
Preload("ReplyTo.Tags")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FilterPostWithCategory(tx *gorm.DB, alias string) *gorm.DB {
|
||||||
|
prefix := viper.GetString("database.prefix")
|
||||||
|
return tx.Joins(fmt.Sprintf("JOIN %spost_categories ON %sposts.id = %spost_categories.post_id", prefix, prefix, prefix)).
|
||||||
|
Joins(fmt.Sprintf("JOIN %scategories ON %scategories.id = %spost_categories.category_id", prefix, prefix, prefix)).
|
||||||
|
Where(fmt.Sprintf("%scategories.alias = ?", prefix), alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FilterPostWithTag(tx *gorm.DB, alias string) *gorm.DB {
|
||||||
|
prefix := viper.GetString("database.prefix")
|
||||||
|
return tx.Joins(fmt.Sprintf("JOIN %spost_tags ON %sposts.id = %spost_tags.post_id", prefix, prefix, prefix)).
|
||||||
|
Joins(fmt.Sprintf("JOIN %stags ON %stags.id = %spost_tags.tag_id", prefix, prefix, prefix)).
|
||||||
|
Where(fmt.Sprintf("%stags.alias = ?", prefix), alias)
|
||||||
|
}
|
||||||
|
|
||||||
func GetPost(tx *gorm.DB) (*models.Post, error) {
|
func GetPost(tx *gorm.DB) (*models.Post, error) {
|
||||||
var post *models.Post
|
var post *models.Post
|
||||||
if err := PreloadRelatedPost(tx).
|
if err := PreloadRelatedPost(tx).
|
||||||
|
@ -13,6 +13,7 @@ export default function PostItem(props: {
|
|||||||
onReply?: (post: any) => void,
|
onReply?: (post: any) => void,
|
||||||
onEdit?: (post: any) => void,
|
onEdit?: (post: any) => void,
|
||||||
onDelete?: (post: any) => void,
|
onDelete?: (post: any) => void,
|
||||||
|
onSearch?: (filter: any) => void,
|
||||||
onError: (message: string | null) => void,
|
onError: (message: string | null) => void,
|
||||||
onReact: () => void
|
onReact: () => void
|
||||||
}) {
|
}) {
|
||||||
@ -72,16 +73,22 @@ export default function PostItem(props: {
|
|||||||
</a>
|
</a>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="mt-2 flex gap-2">
|
<div class="mt-2 mb-5 flex gap-2">
|
||||||
<For each={props.post.categories}>
|
<For each={props.post.categories}>
|
||||||
{item => <a class="link link-primary pb-5">
|
{item =>
|
||||||
#{item.name}
|
<a href={`/search?category=${item.alias}`} class="badge badge-primary">
|
||||||
</a>}
|
<i class="fa-solid fa-layer-group me-1.5"></i>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
</For>
|
</For>
|
||||||
<For each={props.post.tags}>
|
<For each={props.post.tags}>
|
||||||
{item => <a class="link link-primary pb-5">
|
{item =>
|
||||||
#{item.name}
|
<a href={`/search?tag=${item.alias}`} class="badge badge-accent">
|
||||||
</a>}
|
<i class="fa-regular fa-tag me-1.5"></i>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ export default function PostList(props: {
|
|||||||
onRepost?: (post: any) => void,
|
onRepost?: (post: any) => void,
|
||||||
onReply?: (post: any) => void,
|
onReply?: (post: any) => void,
|
||||||
onEdit?: (post: any) => void,
|
onEdit?: (post: any) => void,
|
||||||
onUpdate: (pn: number) => Promise<void>,
|
onUpdate: (pn: number, filter?: any) => Promise<void>,
|
||||||
onError: (message: string | null) => void
|
onError: (message: string | null) => void
|
||||||
}) {
|
}) {
|
||||||
const [loading, setLoading] = createSignal(true);
|
const [loading, setLoading] = createSignal(true);
|
||||||
@ -21,9 +21,9 @@ export default function PostList(props: {
|
|||||||
const [page, setPage] = createSignal(1);
|
const [page, setPage] = createSignal(1);
|
||||||
const pageCount = createMemo(() => Math.ceil(postCount() / 10));
|
const pageCount = createMemo(() => Math.ceil(postCount() / 10));
|
||||||
|
|
||||||
async function readPosts() {
|
async function readPosts(filter?: any) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await props.onUpdate(page());
|
await props.onUpdate(page(), filter);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ render(() => (
|
|||||||
<Route path="/" component={Feed}>
|
<Route path="/" component={Feed}>
|
||||||
<Route path="/" component={Global} />
|
<Route path="/" component={Global} />
|
||||||
<Route path="/posts/:postId" component={PostReference} />
|
<Route path="/posts/:postId" component={PostReference} />
|
||||||
|
<Route path="/search" component={lazy(() => import("./pages/search.tsx"))} />
|
||||||
<Route path="/realms" component={lazy(() => import("./pages/realms"))} />
|
<Route path="/realms" component={lazy(() => import("./pages/realms"))} />
|
||||||
<Route path="/realms/:realmId" component={lazy(() => import("./pages/realms/realm.tsx"))} />
|
<Route path="/realms/:realmId" component={lazy(() => import("./pages/realms/realm.tsx"))} />
|
||||||
<Route path="/accounts/:accountId" component={lazy(() => import("./pages/account.tsx"))} />
|
<Route path="/accounts/:accountId" component={lazy(() => import("./pages/account.tsx"))} />
|
||||||
|
123
pkg/view/src/pages/search.tsx
Normal file
123
pkg/view/src/pages/search.tsx
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { useNavigate, useSearchParams } from "@solidjs/router";
|
||||||
|
import { createSignal, Show } from "solid-js";
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
|
import PostPublish from "../components/PostPublish.tsx";
|
||||||
|
import PostList from "../components/PostList.tsx";
|
||||||
|
import { closeModel, openModel } from "../scripts/modals.ts";
|
||||||
|
|
||||||
|
export default function SearchPage() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const [error, setError] = createSignal<string | null>(null);
|
||||||
|
|
||||||
|
const [page, setPage] = createSignal(0);
|
||||||
|
const [info, setInfo] = createSignal<any>(null);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
async function readPosts(pn?: number) {
|
||||||
|
if (pn) setPage(pn);
|
||||||
|
const res = await fetch("/api/posts?" + new URLSearchParams({
|
||||||
|
take: (10).toString(),
|
||||||
|
offset: ((page() - 1) * 10).toString(),
|
||||||
|
...searchParams
|
||||||
|
}));
|
||||||
|
if (res.status !== 200) {
|
||||||
|
setError(await res.text());
|
||||||
|
} else {
|
||||||
|
setError(null);
|
||||||
|
setInfo(await res.json());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMeta(data: any, field: string, open = true) {
|
||||||
|
const meta: { [id: string]: any } = {
|
||||||
|
reposting: null,
|
||||||
|
replying: null,
|
||||||
|
editing: null
|
||||||
|
};
|
||||||
|
meta[field] = data;
|
||||||
|
setPublishMeta(meta);
|
||||||
|
|
||||||
|
if (open) openModel("#post-publish");
|
||||||
|
else closeModel("#post-publish");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [publishMeta, setPublishMeta] = createStore<any>({
|
||||||
|
replying: null,
|
||||||
|
reposting: null,
|
||||||
|
editing: null
|
||||||
|
});
|
||||||
|
|
||||||
|
function getDescribe() {
|
||||||
|
let builder = [];
|
||||||
|
if (searchParams["category"]) {
|
||||||
|
builder.push("category is #" + searchParams["category"]);
|
||||||
|
} else if (searchParams["tag"]) {
|
||||||
|
builder.push("tag is #" + searchParams["tag"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.join(" and ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function back() {
|
||||||
|
if (window.history.length > 0) {
|
||||||
|
window.history.back();
|
||||||
|
} else {
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="flex pt-1">
|
||||||
|
<button class="btn btn-ghost ml-[20px] w-12 h-12" onClick={() => back()}>
|
||||||
|
<i class="fa-solid fa-angle-left"></i>
|
||||||
|
</button>
|
||||||
|
<div class="px-5 flex items-center">
|
||||||
|
<p>Search</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="alerts">
|
||||||
|
<Show when={error()}>
|
||||||
|
<div role="alert" class="alert alert-error">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span class="capitalize">{error()}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="alert" class="alert alert-info px-[20px]">
|
||||||
|
<i class="fa-solid fa-magnifying-glass pl-[13px]"></i>
|
||||||
|
<span>You will only see posts with <b>{getDescribe()}</b></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dialog id="post-publish" class="modal">
|
||||||
|
<div class="modal-box p-0 w-[540px]">
|
||||||
|
<PostPublish
|
||||||
|
reposting={publishMeta.reposting}
|
||||||
|
replying={publishMeta.replying}
|
||||||
|
editing={publishMeta.editing}
|
||||||
|
onReset={() => setMeta(null, "none", false)}
|
||||||
|
onError={setError}
|
||||||
|
onPost={() => readPosts()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<PostList
|
||||||
|
info={info()}
|
||||||
|
onUpdate={readPosts}
|
||||||
|
onError={setError}
|
||||||
|
onRepost={(item) => setMeta(item, "reposting")}
|
||||||
|
onReply={(item) => setMeta(item, "replying")}
|
||||||
|
onEdit={(item) => setMeta(item, "editing")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user