published toggle
This commit is contained in:
@@ -2,22 +2,7 @@ import { Show } from "solid-js";
|
|||||||
import CardLinks from "./CardLinks";
|
import CardLinks from "./CardLinks";
|
||||||
import DeletePostButton from "./DeletePostButton";
|
import DeletePostButton from "./DeletePostButton";
|
||||||
import { Fire } from "~/components/icons/Fire";
|
import { Fire } from "~/components/icons/Fire";
|
||||||
|
import { Post } from "~/db/types";
|
||||||
export interface Post {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
subtitle: string | null;
|
|
||||||
body: string | null;
|
|
||||||
banner_photo: string | null;
|
|
||||||
date: string;
|
|
||||||
published: boolean;
|
|
||||||
category: string;
|
|
||||||
author_id: string;
|
|
||||||
reads: number;
|
|
||||||
attachments: string | null;
|
|
||||||
total_likes: number;
|
|
||||||
total_comments: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardProps {
|
export interface CardProps {
|
||||||
post: Post;
|
post: Post;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { For, Show, createMemo } from "solid-js";
|
import { For, Show, createMemo } from "solid-js";
|
||||||
import Card, { Post } from "./Card";
|
import Card from "./Card";
|
||||||
|
import { Post } from "~/db/types";
|
||||||
|
|
||||||
export interface Tag {
|
export interface Tag {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -13,10 +14,22 @@ export interface PostSortingProps {
|
|||||||
filters?: string;
|
filters?: string;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
include?: string;
|
include?: string;
|
||||||
|
status?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PostSorting(props: PostSortingProps) {
|
export default function PostSorting(props: PostSortingProps) {
|
||||||
const filteredPosts = createMemo(() => {
|
const filteredPosts = createMemo(() => {
|
||||||
|
let filtered = props.posts;
|
||||||
|
|
||||||
|
// Apply publication status filter (admin only)
|
||||||
|
if (props.privilegeLevel === "admin" && props.status) {
|
||||||
|
if (props.status === "published") {
|
||||||
|
filtered = filtered.filter((post) => post.published === 1);
|
||||||
|
} else if (props.status === "unpublished") {
|
||||||
|
filtered = filtered.filter((post) => post.published === 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build map of post_id -> tags for that post
|
// Build map of post_id -> tags for that post
|
||||||
const postTags = new Map<number, Set<string>>();
|
const postTags = new Map<number, Set<string>>();
|
||||||
props.tags.forEach((tag) => {
|
props.tags.forEach((tag) => {
|
||||||
@@ -41,7 +54,7 @@ export default function PostSorting(props: PostSortingProps) {
|
|||||||
|
|
||||||
const includeSet = new Set(includeList);
|
const includeSet = new Set(includeList);
|
||||||
|
|
||||||
return props.posts.filter((post) => {
|
return filtered.filter((post) => {
|
||||||
const tags = postTags.get(post.id);
|
const tags = postTags.get(post.id);
|
||||||
if (!tags || tags.size === 0) return false;
|
if (!tags || tags.size === 0) return false;
|
||||||
|
|
||||||
@@ -61,12 +74,12 @@ export default function PostSorting(props: PostSortingProps) {
|
|||||||
|
|
||||||
// Empty blacklist means show everything
|
// Empty blacklist means show everything
|
||||||
if (filterList.length === 0) {
|
if (filterList.length === 0) {
|
||||||
return props.posts;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterSet = new Set(filterList);
|
const filterSet = new Set(filterList);
|
||||||
|
|
||||||
return props.posts.filter((post) => {
|
return filtered.filter((post) => {
|
||||||
const tags = postTags.get(post.id);
|
const tags = postTags.get(post.id);
|
||||||
if (!tags || tags.size === 0) return true; // Show posts with no tags
|
if (!tags || tags.size === 0) return true; // Show posts with no tags
|
||||||
|
|
||||||
@@ -81,7 +94,7 @@ export default function PostSorting(props: PostSortingProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No filters: show all posts
|
// No filters: show all posts
|
||||||
return props.posts;
|
return filtered;
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortedPosts = createMemo(() => {
|
const sortedPosts = createMemo(() => {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default function PostSortingSelect(props: PostSortingSelectProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsOpen(!isOpen())}
|
onClick={() => setIsOpen(!isOpen())}
|
||||||
class="focus-visible:border-peach focus-visible:ring-offset-peach bg-surface0 focus-visible:ring-opacity-75 relative w-full cursor-default rounded-lg py-2 pr-10 pl-3 text-left shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 sm:text-sm"
|
class="focus-visible:border-blue focus-visible:ring-offset-blue bg-surface0 focus-visible:ring-opacity-75 relative w-full cursor-default rounded-lg py-2 pr-10 pl-3 text-left shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 sm:text-sm"
|
||||||
>
|
>
|
||||||
<span class="block truncate">{selected().label}</span>
|
<span class="block truncate">{selected().label}</span>
|
||||||
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
@@ -62,7 +62,7 @@ export default function PostSortingSelect(props: PostSortingSelectProps) {
|
|||||||
onClick={() => handleSelect(sort)}
|
onClick={() => handleSelect(sort)}
|
||||||
class={`relative w-full cursor-default py-2 pr-4 pl-10 text-left select-none ${
|
class={`relative w-full cursor-default py-2 pr-4 pl-10 text-left select-none ${
|
||||||
selected().val === sort.val
|
selected().val === sort.val
|
||||||
? "bg-peach text-base brightness-75"
|
? "bg-blue text-base brightness-75"
|
||||||
: "text-text hover:brightness-125"
|
: "text-text hover:brightness-125"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -74,12 +74,12 @@ export default function PostSortingSelect(props: PostSortingSelectProps) {
|
|||||||
{sort.label}
|
{sort.label}
|
||||||
</span>
|
</span>
|
||||||
<Show when={selected().val === sort.val}>
|
<Show when={selected().val === sort.val}>
|
||||||
<span class="text-peach absolute inset-y-0 left-0 flex items-center pl-3">
|
<span class="text-blue absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
<Check
|
<Check
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
height={24}
|
height={24}
|
||||||
width={24}
|
width={24}
|
||||||
class="stroke-text"
|
class="stroke-base"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
45
src/components/blog/PublishStatusToggle.tsx
Normal file
45
src/components/blog/PublishStatusToggle.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useSearchParams } from "@solidjs/router";
|
||||||
|
|
||||||
|
type PublishStatus = "all" | "published" | "unpublished";
|
||||||
|
|
||||||
|
export default function PublishStatusToggle() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const currentStatus = (): PublishStatus => {
|
||||||
|
return (searchParams.status as PublishStatus) || "all";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusChange = (status: PublishStatus) => {
|
||||||
|
setSearchParams({ status });
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonClass = (status: PublishStatus) =>
|
||||||
|
`px-4 py-2 transition-all duration-300 ${
|
||||||
|
currentStatus() === status
|
||||||
|
? "bg-text text-base font-semibold"
|
||||||
|
: "bg-transparent hover:brightness-125"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="border-text mx-auto mt-2 flex overflow-hidden rounded border">
|
||||||
|
<button
|
||||||
|
onClick={() => handleStatusChange("all")}
|
||||||
|
class={buttonClass("all")}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleStatusChange("published")}
|
||||||
|
class={`${buttonClass("published")} border-text border-r border-l`}
|
||||||
|
>
|
||||||
|
Published
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleStatusChange("unpublished")}
|
||||||
|
class={buttonClass("unpublished")}
|
||||||
|
>
|
||||||
|
Unpublished
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ export interface Post {
|
|||||||
body: string;
|
body: string;
|
||||||
banner_photo?: string;
|
banner_photo?: string;
|
||||||
date?: string | null;
|
date?: string | null;
|
||||||
published: boolean;
|
published: number; // 0 or 1 (sqlite)
|
||||||
author_id: string;
|
author_id: string;
|
||||||
reads: number;
|
reads: number;
|
||||||
attachments?: string;
|
attachments?: string;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getRequestEvent } from "solid-js/web";
|
|||||||
import PostSortingSelect from "~/components/blog/PostSortingSelect";
|
import PostSortingSelect from "~/components/blog/PostSortingSelect";
|
||||||
import TagSelector from "~/components/blog/TagSelector";
|
import TagSelector from "~/components/blog/TagSelector";
|
||||||
import PostSorting from "~/components/blog/PostSorting";
|
import PostSorting from "~/components/blog/PostSorting";
|
||||||
|
import PublishStatusToggle from "~/components/blog/PublishStatusToggle";
|
||||||
import { TerminalSplash } from "~/components/TerminalSplash";
|
import { TerminalSplash } from "~/components/TerminalSplash";
|
||||||
|
|
||||||
const getPosts = query(async () => {
|
const getPosts = query(async () => {
|
||||||
@@ -79,6 +80,8 @@ export default function BlogIndex() {
|
|||||||
"filter" in searchParams ? searchParams.filter : undefined;
|
"filter" in searchParams ? searchParams.filter : undefined;
|
||||||
const include = () =>
|
const include = () =>
|
||||||
"include" in searchParams ? searchParams.include : undefined;
|
"include" in searchParams ? searchParams.include : undefined;
|
||||||
|
const status = () =>
|
||||||
|
"status" in searchParams ? searchParams.status : undefined;
|
||||||
|
|
||||||
const data = createAsync(() => getPosts(), { deferStream: true });
|
const data = createAsync(() => getPosts(), { deferStream: true });
|
||||||
|
|
||||||
@@ -90,13 +93,17 @@ export default function BlogIndex() {
|
|||||||
<Show when={data()} fallback={<TerminalSplash />}>
|
<Show when={data()} fallback={<TerminalSplash />}>
|
||||||
{(loadedData) => (
|
{(loadedData) => (
|
||||||
<>
|
<>
|
||||||
<div class="flex flex-row justify-around gap-4 px-4">
|
<div class="flex flex-col items-center gap-4 px-4 md:flex-row md:justify-around">
|
||||||
<PostSortingSelect />
|
<PostSortingSelect />
|
||||||
|
|
||||||
<Show when={Object.keys(loadedData().tagMap).length > 0}>
|
<Show when={Object.keys(loadedData().tagMap).length > 0}>
|
||||||
<TagSelector tagMap={loadedData().tagMap} />
|
<TagSelector tagMap={loadedData().tagMap} />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={loadedData().privilegeLevel === "admin"}>
|
||||||
|
<PublishStatusToggle />
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Show when={loadedData().privilegeLevel === "admin"}>
|
<Show when={loadedData().privilegeLevel === "admin"}>
|
||||||
<div class="mt-2 flex justify-center md:mt-0 md:justify-end">
|
<div class="mt-2 flex justify-center md:mt-0 md:justify-end">
|
||||||
<A
|
<A
|
||||||
@@ -121,6 +128,7 @@ export default function BlogIndex() {
|
|||||||
filters={filters()}
|
filters={filters()}
|
||||||
sort={sort()}
|
sort={sort()}
|
||||||
include={include()}
|
include={include()}
|
||||||
|
status={status()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
Reference in New Issue
Block a user